Compare commits
50 Commits
cce310c2dc
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c8ec7846d | ||
|
|
8d441766c5 | ||
|
|
bf907a1378 | ||
|
|
de6295f2af | ||
|
|
3e0a7b8928 | ||
|
|
09909552bc | ||
|
|
baf57515c0 | ||
|
|
b8c844363c | ||
|
|
87b071c319 | ||
|
|
bdc824e1f6 | ||
|
|
1c7b2d32a6 | ||
|
|
cffd554365 | ||
|
|
33043eedf1 | ||
| ce35f788ca | |||
| 5bbdeb6236 | |||
| 93813f7b56 | |||
|
|
51e6933f9f | ||
|
|
c0b4742966 | ||
| f76c9ed828 | |||
| d50e3781db | |||
| e48e369ef1 | |||
| bb8344b27a | |||
| b538853800 | |||
| f772e3d250 | |||
| f5a7ad51f0 | |||
| 8df8618f40 | |||
|
|
f355835819 | ||
|
|
42c2817c2f | ||
| 9bf0211e78 | |||
| 281272c9f9 | |||
|
|
71cfef996d | ||
|
|
09125378b6 | ||
|
|
e8da956543 | ||
|
|
ad81feaf81 | ||
| 254f61e524 | |||
| 3512d9ae87 | |||
|
|
98e2548f01 | ||
|
|
a6484782a5 | ||
|
|
017d052cd8 | ||
|
|
bd35b3f89f | ||
|
|
66b019eb2a | ||
|
|
4c2edb53e4 | ||
|
|
829f164833 | ||
|
|
45298e5f23 | ||
|
|
7ca69021c4 | ||
|
|
b0ee5a0783 | ||
| 5c931317f0 | |||
| 751cd1aa1f | |||
|
|
80eaa803dd | ||
|
|
7b5dcdacd7 |
@@ -7,14 +7,14 @@
|
|||||||
<!-- <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"> -->
|
<!-- <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"> -->
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=0" />
|
<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=0" />
|
||||||
<link rel="stylesheet" href="/fonts/index.css">
|
<link rel="stylesheet" href="/fonts/index.css">
|
||||||
<title>Lane Crawford</title>
|
<title>Stylish Parade</title>
|
||||||
<!-- Open Graph / WhatsApp share metadata -->
|
<!-- Open Graph / WhatsApp share metadata -->
|
||||||
<meta property="og:title" content="Lane Crawford" />
|
<meta property="og:title" content="Stylish Parade" />
|
||||||
<meta property="og:description" content="create and share looks from the Lane Crawford creation gallery." />
|
<meta property="og:description" content="create and share looks from the Stylish Parade creation gallery." />
|
||||||
<meta property="og:image" content="" />
|
<meta property="og:image" content="" />
|
||||||
<meta property="og:url" content="https://www.lc.aida.com.hk" />
|
<meta property="og:url" content="https://www.lc.aida.com.hk" />
|
||||||
<meta property="og:type" content="website" />
|
<meta property="og:type" content="website" />
|
||||||
<meta property="og:site_name" content="Lane Crawford" />
|
<meta property="og:site_name" content="Stylish Parade" />
|
||||||
<meta name="twitter:card" content="summary_large_image" />
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
<div class="view" ref="viewRef" :style="viewStyle"><RouteCache /></div>
|
<div class="view" ref="viewRef" :style="viewStyle"><RouteCache /></div>
|
||||||
<login-dialog />
|
<login-dialog />
|
||||||
<div id="loading" v-if="loading" v-loading="true"></div>
|
<div id="loading" v-if="loading" v-loading="true"></div>
|
||||||
|
<shopping-drawer />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -11,6 +12,7 @@
|
|||||||
import MainHeader from '@/views/main-header.vue'
|
import MainHeader from '@/views/main-header.vue'
|
||||||
import LoginDialog from '@/views/login/login-dialog.vue'
|
import LoginDialog from '@/views/login/login-dialog.vue'
|
||||||
import { useGlobalStore } from '@/stores'
|
import { useGlobalStore } from '@/stores'
|
||||||
|
import ShoppingDrawer from '@/views/shopping-drawer.vue'
|
||||||
const globalStore = useGlobalStore()
|
const globalStore = useGlobalStore()
|
||||||
const loading = computed(() => globalStore.state.loading)
|
const loading = computed(() => globalStore.state.loading)
|
||||||
const viewRef = ref()
|
const viewRef = ref()
|
||||||
@@ -29,6 +31,12 @@
|
|||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
observer.disconnect()
|
observer.disconnect()
|
||||||
})
|
})
|
||||||
|
window['onClickPrivacy'] = () => {
|
||||||
|
const e = window.event || event
|
||||||
|
e.stopPropagation()
|
||||||
|
e.preventDefault()
|
||||||
|
console.log('点击了隐私政策')
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less">
|
<style lang="less">
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ body,
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
font-family: 'KaiseiOpti-Medium';
|
font-family: 'KaiseiOpti-Medium';
|
||||||
|
color: #232323;
|
||||||
}
|
}
|
||||||
@keyframes loading {
|
@keyframes loading {
|
||||||
0% {
|
0% {
|
||||||
@@ -96,8 +97,11 @@ body,
|
|||||||
--el-color-primary-dark-2: #565656;
|
--el-color-primary-dark-2: #565656;
|
||||||
/* 深灰色(加深20%) */
|
/* 深灰色(加深20%) */
|
||||||
}
|
}
|
||||||
|
.mini-scrollbar {
|
||||||
|
--mini-scrollbar-width: 0.4rem;
|
||||||
|
}
|
||||||
.mini-scrollbar::-webkit-scrollbar {
|
.mini-scrollbar::-webkit-scrollbar {
|
||||||
width: 0.4rem;
|
width: var(--mini-scrollbar-width);
|
||||||
}
|
}
|
||||||
.mini-scrollbar::-webkit-scrollbar-thumb {
|
.mini-scrollbar::-webkit-scrollbar-thumb {
|
||||||
border-radius: 0.4rem;
|
border-radius: 0.4rem;
|
||||||
@@ -107,62 +111,41 @@ body,
|
|||||||
--mosaic-bg-size: 1rem;
|
--mosaic-bg-size: 1rem;
|
||||||
--mosaic-bg-color1: #efefef;
|
--mosaic-bg-color1: #efefef;
|
||||||
--mosaic-bg-color2: #fff;
|
--mosaic-bg-color2: #fff;
|
||||||
background-image: repeating-conic-gradient(
|
background-image: repeating-conic-gradient(var(--mosaic-bg-color1) 0% 25%, var(--mosaic-bg-color2) 0% 50%);
|
||||||
var(--mosaic-bg-color1) 0% 25%,
|
|
||||||
var(--mosaic-bg-color2) 0% 50%
|
|
||||||
);
|
|
||||||
background-repeat: repeat;
|
background-repeat: repeat;
|
||||||
background-position: 50% 50%;
|
background-position: 50% 50%;
|
||||||
background-size: var(--mosaic-bg-size) var(--mosaic-bg-size);
|
background-size: var(--mosaic-bg-size) var(--mosaic-bg-size);
|
||||||
}
|
}
|
||||||
|
|
||||||
.flex {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
.flex-center {
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.flex-1 {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
.flex-col {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
.align-center {
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.space-between {
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
.justify-center {
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
.relative {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
button[custom],
|
button[custom],
|
||||||
button[custom='white'] {
|
button[custom="white"] {
|
||||||
min-width: 19.4rem;
|
min-width: 19.4rem;
|
||||||
height: 5rem;
|
height: 5rem;
|
||||||
padding: 0 1rem;
|
padding: 0 1rem;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
border: none;
|
|
||||||
font-family: KaiseiOpti-Bold;
|
font-family: KaiseiOpti-Bold;
|
||||||
font-size: var(--button-font-size, 2rem);
|
font-size: var(--button-font-size, 2rem);
|
||||||
color: var(--button-color, #232323);
|
color: var(--button-color, #232323);
|
||||||
background: var(--button-bgcolor, #fff);
|
background: var(--button-bgcolor, #fff);
|
||||||
|
border: var(--button-border, none);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
button[custom]:active,
|
button[custom]:active,
|
||||||
button[custom='white']:active {
|
button[custom="white"]:active {
|
||||||
background: var(--button-click-bgcolor, #e4e4e4);
|
background: var(--button-click-bgcolor, #e4e4e4);
|
||||||
color: var(--button-click-color, #232323);
|
color: var(--button-click-color, #232323);
|
||||||
}
|
}
|
||||||
button[custom='black'] {
|
button[custom="black"] {
|
||||||
--button-bgcolor: #232323;
|
--button-bgcolor: #232323;
|
||||||
--button-color: #fff;
|
--button-color: #fff;
|
||||||
--button-click-bgcolor: #333;
|
--button-click-bgcolor: #333;
|
||||||
--button-click-color: #fff;
|
--button-click-color: #fff;
|
||||||
--button-font-size: 1.6rem;
|
--button-font-size: 1.6rem;
|
||||||
}
|
}
|
||||||
|
button[custom="black-box"] {
|
||||||
|
--button-bgcolor: transparent;
|
||||||
|
--button-color: #232323;
|
||||||
|
--button-border: 0.2rem solid #979797;
|
||||||
|
--button-click-bgcolor: #979797;
|
||||||
|
--button-click-color: #fff;
|
||||||
|
--button-font-size: 1.6rem;
|
||||||
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ body,
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
font-family: 'KaiseiOpti-Medium';
|
font-family: 'KaiseiOpti-Medium';
|
||||||
|
color: #232323;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes loading {
|
@keyframes loading {
|
||||||
@@ -118,8 +119,10 @@ body,
|
|||||||
|
|
||||||
// 迷你滚动条
|
// 迷你滚动条
|
||||||
.mini-scrollbar {
|
.mini-scrollbar {
|
||||||
|
--mini-scrollbar-width: 0.4rem;
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
width: 0.4rem;
|
width: var(--mini-scrollbar-width);
|
||||||
}
|
}
|
||||||
|
|
||||||
&::-webkit-scrollbar-thumb {
|
&::-webkit-scrollbar-thumb {
|
||||||
@@ -146,11 +149,11 @@ button[custom="white"] {
|
|||||||
height: 5rem;
|
height: 5rem;
|
||||||
padding: 0 1rem;
|
padding: 0 1rem;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
border: none;
|
|
||||||
font-family: KaiseiOpti-Bold;
|
font-family: KaiseiOpti-Bold;
|
||||||
font-size: var(--button-font-size, 2rem);
|
font-size: var(--button-font-size, 2rem);
|
||||||
color: var(--button-color, #232323);
|
color: var(--button-color, #232323);
|
||||||
background: var(--button-bgcolor, #fff);
|
background: var(--button-bgcolor, #fff);
|
||||||
|
border: var(--button-border, none);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
@@ -166,3 +169,12 @@ button[custom="black"] {
|
|||||||
--button-click-color: #fff;
|
--button-click-color: #fff;
|
||||||
--button-font-size: 1.6rem;
|
--button-font-size: 1.6rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button[custom="black-box"] {
|
||||||
|
--button-bgcolor: transparent;
|
||||||
|
--button-color: #232323;
|
||||||
|
--button-border: 0.2rem solid #979797;
|
||||||
|
--button-click-bgcolor: #979797;
|
||||||
|
--button-click-color: #fff;
|
||||||
|
--button-font-size: 1.6rem;
|
||||||
|
}
|
||||||
6
src/assets/icons/Invoice.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M13.75 5.41675H6.25V6.25008H13.75V5.41675Z" fill="currentColor"/>
|
||||||
|
<path d="M13.75 8.75H6.25V9.58333H13.75V8.75Z" fill="currentColor"/>
|
||||||
|
<path d="M10.4167 12.0833H6.25V12.9166H10.4167V12.0833Z" fill="currentColor"/>
|
||||||
|
<path d="M2.5 18.3334L6.25 16.6667L10 18.3334L13.75 16.6667L17.5 18.3334V1.66675H2.5V18.3334ZM3.33333 2.50008H16.6667V17.0509L14.0883 15.9051L13.75 15.7547L13.4117 15.9051L10 17.4213L6.58833 15.9051L6.25 15.7547L5.91167 15.9051L3.33333 17.0509V2.50008Z" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 602 B |
5
src/assets/icons/brand/delete.svg
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M14.1113 5.26938H12.1387V3.63221C12.1387 2.87065 11.518 2.25 10.7565 2.25H6.73062C5.96906 2.25 5.34841 2.87065 5.34841 3.63221V5.26938H3.37575C3.16774 5.26938 3 5.43713 3 5.64513C3 5.85313 3.16774 6.02087 3.37575 6.02087H4.00646V14.3678C4.00646 15.1293 4.62711 15.75 5.38867 15.75H12.0984C12.86 15.75 13.4806 15.1293 13.4806 14.3678V6.02423H14.1113C14.3193 6.02423 14.4871 5.85649 14.4871 5.64848C14.4871 5.44048 14.3193 5.27274 14.1113 5.27274V5.26938ZM6.73062 3.00485H10.7565C11.102 3.00485 11.3872 3.28666 11.3872 3.63556V5.12848C11.3872 5.209 11.3234 5.27274 11.2429 5.27274H6.24751C6.167 5.27274 6.10325 5.209 6.10325 5.12848V3.63556C6.10325 3.29001 6.38506 3.00485 6.73397 3.00485H6.73062ZM12.0984 15.0019H5.38867C5.04312 15.0019 4.75795 14.7201 4.75795 14.3711V6.02423H12.7258V14.3711C12.7258 14.7167 12.444 15.0019 12.0951 15.0019H12.0984Z" fill="#979797"/>
|
||||||
|
<path d="M7.40098 7.95319C7.15943 7.95319 6.96484 8.14777 6.96484 8.38932V12.2977C6.96484 12.5393 7.15943 12.7339 7.40098 12.7339C7.64253 12.7339 7.83711 12.5393 7.83711 12.2977V8.38932C7.83711 8.14777 7.64253 7.95319 7.40098 7.95319Z" fill="#979797"/>
|
||||||
|
<path d="M10.0846 7.95319C9.84302 7.95319 9.64844 8.14777 9.64844 8.38932V12.2977C9.64844 12.5393 9.84302 12.7339 10.0846 12.7339C10.3261 12.7339 10.5207 12.5393 10.5207 12.2977V8.38932C10.5207 8.14777 10.3261 7.95319 10.0846 7.95319Z" fill="#979797"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.4 KiB |
5
src/assets/icons/brand/more.svg
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M9.5 12.5C9.5 13.8805 10.6195 15 12 15C13.3805 15 14.5 13.8805 14.5 12.5C14.5 11.1195 13.3805 10 12 10C10.6195 10 9.5 11.1195 9.5 12.5ZM13.5 12.5C13.5 13.327 12.827 14 12 14C11.173 14 10.5 13.327 10.5 12.5C10.5 11.673 11.173 11 12 11C12.827 11 13.5 11.673 13.5 12.5Z" fill="#7B7B7B"/>
|
||||||
|
<path d="M3 12.5C3 13.8805 4.1195 15 5.5 15C6.8805 15 8 13.8805 8 12.5C8 11.1195 6.8805 10 5.5 10C4.1195 10 3 11.1195 3 12.5ZM7 12.5C7 13.327 6.327 14 5.5 14C4.673 14 4 13.327 4 12.5C4 11.673 4.673 11 5.5 11C6.327 11 7 11.673 7 12.5Z" fill="#7B7B7B"/>
|
||||||
|
<path d="M16 12.5C16 13.8805 17.1195 15 18.5 15C19.8805 15 21 13.8805 21 12.5C21 11.1195 19.8805 10 18.5 10C17.1195 10 16 11.1195 16 12.5ZM20 12.5C20 13.327 19.327 14 18.5 14C17.673 14 17 13.327 17 12.5C17 11.673 17.673 11 18.5 11C19.327 11 20 11.673 20 12.5Z" fill="#7B7B7B"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 927 B |
4
src/assets/icons/brand/search.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="13.8654" cy="13.8665" r="7.86667" stroke="#232323" stroke-width="1.33333"/>
|
||||||
|
<path d="M19.5586 19.5552L26.6697 26.6663" stroke="#232323" stroke-width="1.33333" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 299 B |
3
src/assets/icons/brand/time.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M10 20C4.48583 20 0 15.5142 0 10C0 4.48583 4.48583 0 10 0C15.5142 0 20 4.48583 20 10C20 15.5142 15.5142 20 10 20ZM10 0.833333C4.94583 0.833333 0.833333 4.94583 0.833333 10C0.833333 15.0542 4.94583 19.1667 10 19.1667C15.0542 19.1667 19.1667 15.0542 19.1667 10C19.1667 4.94583 15.0542 0.833333 10 0.833333ZM12.3333 14.0833C12.5175 13.945 12.555 13.6842 12.4175 13.5L10.0008 10.2775V4.58333C10.0008 4.35333 9.81417 4.16667 9.58417 4.16667C9.35417 4.16667 9.1675 4.35333 9.1675 4.58333V10.4167C9.1675 10.5067 9.19667 10.5942 9.25083 10.6667L11.7508 14C11.8333 14.1092 11.9583 14.1667 12.0842 14.1667C12.1708 14.1667 12.2583 14.1392 12.3333 14.0833Z" fill="black"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 773 B |
5
src/assets/icons/digital/Info.svg
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M11.995 2C6.48087 2 2 6.48584 2 12C2 17.5142 6.48584 22 11.995 22C17.5042 22 21.9901 17.5142 21.9901 12C21.9901 6.48584 17.5092 2 11.995 2ZM11.995 20.8823C7.09687 20.8823 3.11277 16.8982 3.11277 12C3.11277 7.10184 7.09687 3.11773 11.995 3.11773C16.8932 3.11773 20.8773 7.10184 20.8773 12C20.8773 16.8982 16.8932 20.8823 11.995 20.8823Z" fill="#585858"/>
|
||||||
|
<path d="M12.6169 11.0065C12.6169 10.6636 12.3389 10.3856 11.996 10.3856C11.653 10.3856 11.375 10.6636 11.375 11.0065V16.9678C11.375 17.3107 11.653 17.5887 11.996 17.5887C12.3389 17.5887 12.6169 17.3107 12.6169 16.9678V11.0065Z" fill="#585858"/>
|
||||||
|
<path d="M11.9935 8.02584C12.54 8.02584 12.9871 7.57875 12.9871 7.0323C12.9871 6.48585 12.54 6.03876 11.9935 6.03876C11.4471 6.03876 11 6.48585 11 7.0323C11 7.57875 11.4471 8.02584 11.9935 8.02584Z" fill="#585858"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 928 B |
4
src/assets/icons/digital/back.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect x="0.583333" y="0.583333" width="26.8333" height="26.8333" stroke="#232323" stroke-width="1.16667"/>
|
||||||
|
<path d="M17.2441 19.3639C17.4438 19.5645 17.4438 19.8889 17.2441 20.0895C17.0442 20.2903 16.7205 20.2903 16.5205 20.0895L10.709 14.2506L10.707 14.2477C10.5752 14.1088 10.5718 13.8877 10.709 13.7496L16.5205 7.91077C16.7205 7.70981 17.0441 7.70981 17.2441 7.91077C17.4438 8.11136 17.4438 8.43576 17.2441 8.63635L12.0029 13.902V13.9078L11.9639 13.9489C11.9326 13.982 11.9358 14.0298 11.9619 14.0563L17.2441 19.3639Z" fill="#232323" stroke="#232323" stroke-width="0.28"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 679 B |
8
src/assets/icons/digital/document.svg
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M14 3.5C17.0376 3.5 19.5 5.96243 19.5 9V20.5H4.5V3.5H14Z" stroke="#585858" stroke-linejoin="round"/>
|
||||||
|
<path d="M14.5 3.5C14.5 8 14.7 8 19.5 8" stroke="#585858"/>
|
||||||
|
<path d="M8 11H16" stroke="#585858" stroke-linecap="round"/>
|
||||||
|
<path d="M8 8H12" stroke="#585858" stroke-linecap="round"/>
|
||||||
|
<path d="M8 14H16" stroke="#585858" stroke-linecap="round"/>
|
||||||
|
<path d="M8 17H16" stroke="#585858" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 516 B |
5
src/assets/icons/digital/pullIcon.svg
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g opacity="0.5">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.82684 8.7465L7.13386 6.43947L9.44089 8.7465C9.67278 8.97839 10.0474 8.97839 10.2793 8.7465C10.5112 8.51461 10.5112 8.14001 10.2793 7.90812L7.55008 5.17893C7.31819 4.94704 6.94359 4.94704 6.7117 5.17893L3.98251 7.90812C3.75062 8.14001 3.75062 8.51461 3.98251 8.7465C4.2144 8.97244 4.59495 8.97839 4.82684 8.7465Z" fill="black"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 506 B |
3
src/assets/icons/download.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="26" height="20" viewBox="0 0 26 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M0 18.6667H25.3333V20H0V18.6667ZM18 8.39067L13.3333 13.0573V0H12V13.0573L7.33333 8.39067L6.39067 9.33333L12.6667 15.6093L18.9427 9.33333L18 8.39067Z" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 284 B |
4
src/assets/icons/downloadBtn.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">
|
||||||
|
<path d="M6.32243 7.42574L9.409 10.5123C9.43299 10.5363 9.46897 10.5443 9.50096 10.5323C9.53294 10.5203 9.55293 10.4883 9.55293 10.4563V2.94781C9.55293 2.69992 9.75284 2.50002 10.0007 2.50002C10.2486 2.50002 10.4485 2.69992 10.4485 2.94781V10.4563C10.4485 10.4923 10.4685 10.5203 10.5005 10.5323C10.5325 10.5443 10.5685 10.5403 10.5925 10.5123L13.679 7.42574C13.851 7.25782 14.1468 7.25782 14.3147 7.42574C14.3987 7.5097 14.4467 7.62565 14.4467 7.74559C14.4467 7.86554 14.3987 7.97749 14.3147 8.06545L10.1287 12.2515C10.0607 12.3195 9.93676 12.3195 9.86879 12.2515L5.68272 8.06545C5.59876 7.98148 5.55078 7.86554 5.55078 7.74559C5.55078 7.62565 5.59876 7.5137 5.68272 7.42574C5.85064 7.25782 6.1505 7.25782 6.31843 7.42574H6.32243Z" fill="currentColor"/>
|
||||||
|
<path d="M17.1966 12.6062C16.9487 12.6062 16.7488 12.8061 16.7488 13.054V16.2525C16.7488 16.4444 16.5929 16.6044 16.397 16.6044H3.60289C3.41098 16.6044 3.25106 16.4484 3.25106 16.2525V13.054C3.25106 12.8061 3.05115 12.6062 2.80326 12.6062C2.55538 12.6062 2.35547 12.8061 2.35547 13.054V16.2525C2.35547 16.9402 2.91521 17.4999 3.60289 17.4999H16.397C17.0847 17.4999 17.6444 16.9402 17.6444 16.2525V13.054C17.6444 12.8061 17.4445 12.6062 17.1966 12.6062Z" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
5
src/assets/icons/order/delete.svg
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M15.678 5.85487H13.4861V4.03579C13.4861 3.18961 12.7965 2.5 11.9503 2.5H7.47716C6.63099 2.5 5.94138 3.18961 5.94138 4.03579V5.85487H3.74953C3.51841 5.85487 3.33203 6.04125 3.33203 6.27237C3.33203 6.50348 3.51841 6.68986 3.74953 6.68986H4.45032V15.9642C4.45032 16.8104 5.13993 17.5 5.98611 17.5H13.4414C14.2875 17.5 14.9772 16.8104 14.9772 15.9642V6.69359H15.678C15.9091 6.69359 16.0954 6.50721 16.0954 6.27609C16.0954 6.04498 15.9091 5.8586 15.678 5.8586V5.85487ZM7.47716 3.33872H11.9503C12.3343 3.33872 12.6511 3.65184 12.6511 4.03951V5.69831C12.6511 5.78777 12.5803 5.8586 12.4908 5.8586H6.94038C6.85092 5.8586 6.78009 5.78777 6.78009 5.69831V4.03951C6.78009 3.65557 7.09321 3.33872 7.48089 3.33872H7.47716ZM13.4414 16.6687H5.98611C5.60216 16.6687 5.28531 16.3556 5.28531 15.9679V6.69359H14.1384V15.9679C14.1384 16.3519 13.8253 16.6687 13.4376 16.6687H13.4414Z" fill="#808080"/>
|
||||||
|
<path d="M8.22287 8.83685C7.95448 8.83685 7.73828 9.05306 7.73828 9.32145V13.6641C7.73828 13.9325 7.95448 14.1487 8.22287 14.1487C8.49126 14.1487 8.70747 13.9325 8.70747 13.6641V9.32145C8.70747 9.05306 8.49126 8.83685 8.22287 8.83685Z" fill="#808080"/>
|
||||||
|
<path d="M11.2072 8.83685C10.9389 8.83685 10.7227 9.05306 10.7227 9.32145V13.6641C10.7227 13.9325 10.9389 14.1487 11.2072 14.1487C11.4756 14.1487 11.6918 13.9325 11.6918 13.6641V9.32145C11.6918 9.05306 11.4756 8.83685 11.2072 8.83685Z" fill="#808080"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
8
src/assets/icons/order/file.svg
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<svg width="16" height="18" viewBox="0 0 16 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M10 0.5C13.0376 0.5 15.5 2.96243 15.5 6V17.5H0.5V0.5H10Z" stroke="#232323" stroke-linejoin="round"/>
|
||||||
|
<path d="M10.5 0.5C10.5 5 10.7 5 15.5 5" stroke="#232323"/>
|
||||||
|
<path d="M4 8H12" stroke="#232323" stroke-linecap="round"/>
|
||||||
|
<path d="M4 5H8" stroke="#232323" stroke-linecap="round"/>
|
||||||
|
<path d="M4 11H12" stroke="#232323" stroke-linecap="round"/>
|
||||||
|
<path d="M4 14H12" stroke="#232323" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 514 B |
5
src/assets/icons/order/shop.svg
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M20 6.4C20 5.3 19.1 4 18 4H7.5C7.4 4 7.2 4 7.1 4C5.9 4.2 5 5.2 5 6.4L4 9C4 10.4 5.1 11.5 6.5 11.5C7.9 11.5 8.1 11.1 8.5 10.5C8.9 11.1 9.7 11.5 10.5 11.5C11.3 11.5 12.1 11.1 12.5 10.5C12.9 11.1 13.7 11.5 14.5 11.5C15.3 11.5 16.1 11.1 16.5 10.5C16.9 11.1 17.7 11.5 18.5 11.5C19.9 11.5 21 10.4 21 9M5.9 6.4C5.9 5.6 6.5 5 7.2 4.9C7.2 4.9 7.3 4.9 7.4 4.9H17.9C18.5 4.9 19 5.8 19 6.4C19 7 20 9.1 20 9.1C20 10 19.3 10.7 18.4 10.7C17.5 10.7 16.8 10 16.8 9.1V7.6H15.9V9.1C15.9 10 15.2 10.7 14.3 10.7C13.4 10.7 12.7 10 12.7 9.1V7.6H11.8V9.1C11.8 10 11.1 10.7 10.2 10.7C9.3 10.7 8.6 10 8.6 9.1V7.6H7.7V9.1C7.7 10 7.50938 10.7 6.60938 10.7C5.48438 10.7 4.9 9.9 4.9 9L5.9 6.4Z" fill="#232323"/>
|
||||||
|
<path d="M6 11.2V19.3C6 19.9 6.4 20.3 7 20.3H18C18.6 20.3 19 19.9 19 19.3V13.5" stroke="#232323" stroke-linecap="round"/>
|
||||||
|
<path d="M17 15.5V13.5C17 13.2239 16.7761 13 16.5 13C16.2239 13 16 13.2239 16 13.5V15.5C16 15.7761 16.2239 16 16.5 16C16.7761 16 17 15.7761 17 15.5Z" fill="#232323"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
5
src/assets/icons/shop.svg
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M20 6.4C20 5.3 19.1 4 18 4H7.5C7.4 4 7.2 4 7.1 4C5.9 4.2 5 5.2 5 6.4L4 9C4 10.4 5.1 11.5 6.5 11.5C7.9 11.5 8.1 11.1 8.5 10.5C8.9 11.1 9.7 11.5 10.5 11.5C11.3 11.5 12.1 11.1 12.5 10.5C12.9 11.1 13.7 11.5 14.5 11.5C15.3 11.5 16.1 11.1 16.5 10.5C16.9 11.1 17.7 11.5 18.5 11.5C19.9 11.5 21 10.4 21 9M5.9 6.4C5.9 5.6 6.5 5 7.2 4.9C7.2 4.9 7.3 4.9 7.4 4.9H17.9C18.5 4.9 19 5.8 19 6.4C19 7 20 9.1 20 9.1C20 10 19.3 10.7 18.4 10.7C17.5 10.7 16.8 10 16.8 9.1V7.6H15.9V9.1C15.9 10 15.2 10.7 14.3 10.7C13.4 10.7 12.7 10 12.7 9.1V7.6H11.8V9.1C11.8 10 11.1 10.7 10.2 10.7C9.3 10.7 8.6 10 8.6 9.1V7.6H7.7V9.1C7.7 10 7.50938 10.7 6.60938 10.7C5.48438 10.7 4.9 9.9 4.9 9L5.9 6.4Z" fill="#232323"/>
|
||||||
|
<path d="M6 11.2V19.3C6 19.9 6.4 20.3 7 20.3H18C18.6 20.3 19 19.9 19 19.3V13.5" stroke="#232323" stroke-width="0.75" stroke-linecap="round"/>
|
||||||
|
<path d="M17 15.5V13.5C17 13.2239 16.7761 13 16.5 13C16.2239 13 16 13.2239 16 13.5V15.5C16 15.7761 16.2239 16 16.5 16C16.7761 16 17 15.7761 17 15.5Z" fill="#232323"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
4
src/assets/icons/statement.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M8.32919 0C3.73406 0 0 3.7382 0 8.33333C0 12.9285 3.7382 16.6667 8.32919 16.6667C12.9202 16.6667 16.6584 12.9285 16.6584 8.33333C16.6584 3.7382 12.9243 0 8.32919 0ZM8.32919 15.7352C4.24739 15.7352 0.927306 12.4151 0.927306 8.33333C0.927306 4.25153 4.24739 0.931445 8.32919 0.931445C12.411 0.931445 15.7311 4.25153 15.7311 8.33333C15.7311 12.4151 12.411 15.7352 8.32919 15.7352Z" fill="#979797"/>
|
||||||
|
<path d="M8.84744 7.50551C8.84744 7.21972 8.61576 6.98804 8.32997 6.98804C8.04418 6.98804 7.8125 7.21972 7.8125 7.50551V12.4732C7.8125 12.759 8.04418 12.9907 8.32997 12.9907C8.61576 12.9907 8.84744 12.759 8.84744 12.4732V7.50551Z" fill="#979797"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 756 B |
BIN
src/assets/images/account/account-bg.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
src/assets/images/account/designer-lian-su.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
src/assets/images/account/item-01.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
src/assets/images/account/item-02.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
src/assets/images/account/item-03.png
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
BIN
src/assets/images/account/item-04.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
src/assets/images/account/item-05.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
src/assets/images/account/item-06.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
src/assets/images/account/item-07.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
src/assets/images/account/item-08.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
src/assets/images/account/item-09.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
src/assets/images/background.png
Normal file
|
After Width: | Height: | Size: 2.4 MiB |
BIN
src/assets/images/brand-null.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 713 KiB After Width: | Height: | Size: 597 KiB |
BIN
src/assets/images/brand/brandDetailBg.png
Normal file
|
After Width: | Height: | Size: 713 KiB |
BIN
src/assets/images/brand/brandLoading.gif
Normal file
|
After Width: | Height: | Size: 133 KiB |
BIN
src/assets/images/collectionStory/code-create.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 1.7 MiB After Width: | Height: | Size: 808 KiB |
BIN
src/assets/images/digitalItem/digital_item_banner.png
Normal file
|
After Width: | Height: | Size: 615 KiB |
|
Before Width: | Height: | Size: 851 KiB |
BIN
src/assets/images/home/bg.png
Normal file
|
After Width: | Height: | Size: 652 KiB |
BIN
src/assets/images/home/designer-bg.png
Normal file
|
After Width: | Height: | Size: 688 KiB |
|
Before Width: | Height: | Size: 569 KiB |
|
Before Width: | Height: | Size: 151 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
BIN
src/assets/images/shopping-cart-null.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
src/assets/images/wardrobe/checked.png
Normal file
|
After Width: | Height: | Size: 317 B |
BIN
src/assets/images/wardrobe/empty-wardrobe.png
Normal file
|
After Width: | Height: | Size: 8.8 KiB |
BIN
src/assets/images/wardrobe/select.png
Normal file
|
After Width: | Height: | Size: 208 B |
@@ -1,82 +1,104 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, onUnmounted, reactive, toRefs } from "vue";
|
import { ref, onMounted, onUnmounted, reactive, toRefs } from 'vue'
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
url: {
|
url: {
|
||||||
type: String,
|
type: String,
|
||||||
default: ''
|
default: ''
|
||||||
},
|
},
|
||||||
name: {
|
name: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'aaa'
|
default: 'aaa'
|
||||||
},
|
},
|
||||||
price: {
|
price: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '111'
|
default: '111'
|
||||||
}
|
},
|
||||||
})
|
download: {
|
||||||
const emit = defineEmits([
|
type: Boolean,
|
||||||
'addShopping'
|
default: false
|
||||||
])
|
},
|
||||||
let data = reactive({
|
showPrice: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
const emit = defineEmits(['addShopping', 'openDetail', 'download'])
|
||||||
|
let data = reactive({})
|
||||||
const addShopping = () => {
|
const addShopping = () => {
|
||||||
emit('addShopping')
|
if (props.download) {
|
||||||
|
emit('download')
|
||||||
|
} else {
|
||||||
|
emit('addShopping')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
const openDetail = () => {
|
||||||
onMounted(()=>{
|
emit('openDetail')
|
||||||
})
|
}
|
||||||
onUnmounted(()=>{
|
onMounted(() => {})
|
||||||
})
|
onUnmounted(() => {})
|
||||||
defineExpose({})
|
defineExpose({})
|
||||||
const {} = toRefs(data);
|
const {} = toRefs(data)
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div class="commodity-item">
|
<div class="commodity-item" :class="{ 'is-download': download }">
|
||||||
<img :src="props.url" alt="">
|
<img :src="props.url" alt="" @click="openDetail" />
|
||||||
<div class="detail">
|
<div class="detail">
|
||||||
<div calss="text">
|
<div class="text">
|
||||||
<div class="name">
|
<div class="name">
|
||||||
{{ props.name }}
|
{{ props.name }}
|
||||||
</div>
|
</div>
|
||||||
<div class="price">
|
<div class="price" :class="{ 'is-download': download }" v-if="props.showPrice">
|
||||||
{{ props.price }}
|
{{ props.price }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="btn" @click="addShopping">
|
<div class="btn" @click="addShopping">
|
||||||
<div class="text">
|
<div class="text">
|
||||||
<SvgIcon name="add" size="24"></SvgIcon>
|
<SvgIcon :name="download ? 'download' : 'add'" size="26" color="#232323"></SvgIcon>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
.commodity-item{
|
.commodity-item {
|
||||||
width: var(--commodity-width,100%);
|
width: var(--commodity-width, 100%);
|
||||||
> img{
|
&.is-download{
|
||||||
width: 100%;
|
img{
|
||||||
height: var(--commodity-height,auto);
|
cursor: initial;
|
||||||
margin-bottom: var(--commodity-marginBottom,1rem);
|
}
|
||||||
}
|
}
|
||||||
> .detail{
|
> img {
|
||||||
display: flex;
|
width: 100%;
|
||||||
justify-content: space-between;
|
cursor: pointer;
|
||||||
align-items: center;
|
height: var(--commodity-height, auto);
|
||||||
.text{
|
margin-bottom: var(--commodity-marginBottom, 1rem);
|
||||||
color: #232323;
|
}
|
||||||
> .name{
|
> .detail {
|
||||||
font-family: "KaiseiOpti-Regular";
|
display: flex;
|
||||||
font-weight: 400;
|
justify-content: space-between;
|
||||||
font-size: var(--commodity-name-fontSize,1.6rem);
|
align-items: center;
|
||||||
line-height: var(--commodity-name-lineHeight,2.3rem);
|
> .text {
|
||||||
}
|
color: #232323;
|
||||||
> .price{
|
> .name {
|
||||||
font-family: "KaiseiOpti-Regular";
|
font-family: 'KaiseiOpti-Regular';
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-size: var(--commodity-price-fontSize,1.4rem);
|
font-size: var(--commodity-name-fontSize, 1.6rem);
|
||||||
line-height: var(--commodity-price-lineHeight,2.3rem);
|
line-height: var(--commodity-name-lineHeight, 2.3rem);
|
||||||
}
|
margin-bottom: var(--commodity-name-marginBottom, 0rem);
|
||||||
}
|
}
|
||||||
}
|
> .price {
|
||||||
}
|
font-family: 'KaiseiOpti-Regular';
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: var(--commodity-price-fontSize, 1.4rem);
|
||||||
|
line-height: var(--commodity-price-lineHeight, 2.3rem);
|
||||||
|
&.is-download {
|
||||||
|
color: #979797;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.btn {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
87
src/components/checked.vue
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, onUnmounted, reactive, toRefs, computed } from "vue";
|
||||||
|
const props = defineProps({
|
||||||
|
list:{
|
||||||
|
type:Array,
|
||||||
|
default:()=>[]
|
||||||
|
},
|
||||||
|
selected:{
|
||||||
|
type:String,
|
||||||
|
default:()=>''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const emit = defineEmits([
|
||||||
|
'update:selected'
|
||||||
|
])
|
||||||
|
const checkList = computed(()=>{
|
||||||
|
if(props.selected[0] === ''){
|
||||||
|
return props.list.map(item => item.value)
|
||||||
|
}else{
|
||||||
|
return [...props.selected]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const handleChange = (val) => {
|
||||||
|
emit('update:selected', val)
|
||||||
|
}
|
||||||
|
const checkAll = computed(()=>{
|
||||||
|
return checkList.value.length === props.list.length
|
||||||
|
})
|
||||||
|
const handleCheckAllChange = (val) => {
|
||||||
|
if(val){
|
||||||
|
emit('update:selected', props.list.map(item => item.value))
|
||||||
|
}else{
|
||||||
|
emit('update:selected', [])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let data = reactive({
|
||||||
|
})
|
||||||
|
onMounted(()=>{
|
||||||
|
})
|
||||||
|
onUnmounted(()=>{
|
||||||
|
})
|
||||||
|
defineExpose({})
|
||||||
|
const {} = toRefs(data);
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="all">
|
||||||
|
<el-checkbox
|
||||||
|
v-model="checkAll"
|
||||||
|
@change="handleCheckAllChange"
|
||||||
|
>
|
||||||
|
All
|
||||||
|
</el-checkbox>
|
||||||
|
</div>
|
||||||
|
<el-checkbox-group v-model="checkList" @change="handleChange">
|
||||||
|
<el-checkbox
|
||||||
|
v-for="item in props.list"
|
||||||
|
:key="item.value"
|
||||||
|
:value="item.value"
|
||||||
|
>
|
||||||
|
{{ item.label }}
|
||||||
|
</el-checkbox>
|
||||||
|
</el-checkbox-group>
|
||||||
|
</template>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.all{
|
||||||
|
margin-bottom: 1.2rem;
|
||||||
|
}
|
||||||
|
.el-checkbox-group{
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.2rem;
|
||||||
|
}
|
||||||
|
label{
|
||||||
|
--el-checkbox-font-size: 1.6rem;
|
||||||
|
--el-checkbox-checked-text-color: #232323;
|
||||||
|
--el-checkbox-font-weight: 400;
|
||||||
|
--el-checkbox-height: 2rem;
|
||||||
|
--el-checkbox-checked-bg-color: #232323;
|
||||||
|
--el-checkbox-checked-input-border-color: #232323;
|
||||||
|
--el-checkbox-input-border: 1px solid #232323;
|
||||||
|
font-family: "KaiseiOpti-Regular";
|
||||||
|
line-height: 2rem;
|
||||||
|
.el-checkbox__label{
|
||||||
|
padding-left: 1.4rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -2,8 +2,7 @@ export default {
|
|||||||
Login: {
|
Login: {
|
||||||
login: 'Log in',
|
login: 'Log in',
|
||||||
register: 'Register',
|
register: 'Register',
|
||||||
loginTo: 'Log on to <span>FiDA</span>',
|
loginTip: 'Platform integrated with AiDA.<br />AiDA account login required.',
|
||||||
loginTitle: 'A multi-agent canvas for rapid, trend driven design iteration.',
|
|
||||||
name: 'Name',
|
name: 'Name',
|
||||||
email: 'Email',
|
email: 'Email',
|
||||||
password: 'Password',
|
password: 'Password',
|
||||||
@@ -40,4 +39,93 @@ export default {
|
|||||||
sendCodeError: 'Send code error',
|
sendCodeError: 'Send code error',
|
||||||
retrievePassword: 'Retrieve password'
|
retrievePassword: 'Retrieve password'
|
||||||
},
|
},
|
||||||
|
Settings: {
|
||||||
|
title: 'Settings',
|
||||||
|
slogan: 'Manage your account settings and preferences',
|
||||||
|
profile: {
|
||||||
|
title: 'Profile',
|
||||||
|
description: 'Update your display name, avatar, social links and account security.',
|
||||||
|
firstName: 'FIRST NAME',
|
||||||
|
lastName: 'LAST NAME',
|
||||||
|
firstNamePlaceholder: 'First Name',
|
||||||
|
lastNamePlaceholder: 'Last Name',
|
||||||
|
username: 'USERNAME',
|
||||||
|
usernamePlaceholder: 'Username',
|
||||||
|
usernameTip: 'Your public username on Stylish Parade.',
|
||||||
|
role: 'ROLE',
|
||||||
|
roleTip: 'Select up to 2 labels that suit you.',
|
||||||
|
},
|
||||||
|
security: {
|
||||||
|
title: 'Security',
|
||||||
|
description: 'Manage your login email and password.',
|
||||||
|
email: 'EMAIL',
|
||||||
|
newEmail: 'NEW EMAIL ADDRESS',
|
||||||
|
newEmailPlaceholder: 'Enter new email',
|
||||||
|
verify: 'Verify',
|
||||||
|
verified: 'Verified',
|
||||||
|
verifiedTip: 'Your new email has been verified and is ready to save.',
|
||||||
|
password: 'PASSWORD',
|
||||||
|
newPassword: 'NEW PASSWORD',
|
||||||
|
newPasswordPlaceholder: 'Enter new password',
|
||||||
|
passwordTip: 'You must satisfy ALL password conditions to register.',
|
||||||
|
currentPassword: 'CURRENT PASSWORD',
|
||||||
|
currentPasswordPlaceholder: 'Confirm with your password'
|
||||||
|
},
|
||||||
|
region: {
|
||||||
|
title: 'Language & Region',
|
||||||
|
description: 'Set your preferred language, region and currency display.',
|
||||||
|
displayLanguage: 'DISPLAY LANGUAGE',
|
||||||
|
selectLanguage: 'Select language',
|
||||||
|
region: 'REGION',
|
||||||
|
selectRegion: 'Select region'
|
||||||
|
},
|
||||||
|
buttons: {
|
||||||
|
cancel: 'CANCEL',
|
||||||
|
discard: 'DISCARD',
|
||||||
|
edit: 'EDIT',
|
||||||
|
saveChange: 'SAVE CHANGE',
|
||||||
|
saving: 'SAVING...'
|
||||||
|
},
|
||||||
|
dialog: {
|
||||||
|
title: 'Check your new email',
|
||||||
|
subtitle: 'Enter the 6-digit code sent to',
|
||||||
|
submit: 'Submit',
|
||||||
|
resendCode: 'Resend Code',
|
||||||
|
resendCodeIn: 'Resend Code in {time}'
|
||||||
|
},
|
||||||
|
messages: {
|
||||||
|
enterNewEmailFirst: 'Please enter your new email address first',
|
||||||
|
invalidEmail: 'Please enter a valid email address',
|
||||||
|
sameEmail: 'Please enter a different email address',
|
||||||
|
alreadyVerified: 'This email has already been verified',
|
||||||
|
verificationCodeSent: 'Verification code sent',
|
||||||
|
enterVerificationCode: 'Please enter the 6-digit verification code',
|
||||||
|
verificationCompleted: 'Email verification completed',
|
||||||
|
verifyEmailBeforeSave: 'Please verify your new email before saving',
|
||||||
|
settingsUpdated: 'Settings updated'
|
||||||
|
},
|
||||||
|
roles: {
|
||||||
|
fashionEnthusiast: 'Fashion Enthusiast',
|
||||||
|
contentCreator: 'Content Creator',
|
||||||
|
student: 'Student',
|
||||||
|
retailBuyer: 'Retail / Buyer',
|
||||||
|
fashionDesigner: 'Fashion Designer',
|
||||||
|
brandBusiness: 'Brand / Business',
|
||||||
|
prCommunications: 'PR & Communications',
|
||||||
|
stylist: 'Stylist',
|
||||||
|
graphicDesigner: 'Graphic Designer',
|
||||||
|
artist3d: '3D Artist',
|
||||||
|
other: 'Other'
|
||||||
|
},
|
||||||
|
languages: {
|
||||||
|
english: 'English',
|
||||||
|
chinese: 'Chinese',
|
||||||
|
},
|
||||||
|
regions: {
|
||||||
|
hongKongSar: 'Hong Kong SAR',
|
||||||
|
mainlandChina: 'Mainland China',
|
||||||
|
singapore: 'Singapore',
|
||||||
|
unitedKingdom: 'United Kingdom'
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,7 @@ export default {
|
|||||||
Login: {
|
Login: {
|
||||||
login: '登录',
|
login: '登录',
|
||||||
register: '注册',
|
register: '注册',
|
||||||
signUp: '注册',
|
loginTip: '与 AiDA 集成的平台。<br />需要登录 AiDA 账户。',
|
||||||
loginTo: '登录到 <span>FiDA</span',
|
|
||||||
loginTitle: '一个多智能体画布,用于快速、趋势驱动的设计迭代。',
|
|
||||||
name: '姓名',
|
name: '姓名',
|
||||||
email: '邮箱',
|
email: '邮箱',
|
||||||
password: '密码',
|
password: '密码',
|
||||||
@@ -41,4 +39,93 @@ export default {
|
|||||||
sendCodeError: '发送验证码失败',
|
sendCodeError: '发送验证码失败',
|
||||||
retrievePassword: '找回密码'
|
retrievePassword: '找回密码'
|
||||||
},
|
},
|
||||||
|
Settings: {
|
||||||
|
title: '设置',
|
||||||
|
slogan: '管理你的账户设置和偏好',
|
||||||
|
profile: {
|
||||||
|
title: '个人资料',
|
||||||
|
description: '更新你的显示名称、头像、社交链接和账户安全信息。',
|
||||||
|
firstName: '名字',
|
||||||
|
lastName: '姓氏',
|
||||||
|
firstNamePlaceholder: '请输入名字',
|
||||||
|
lastNamePlaceholder: '请输入姓氏',
|
||||||
|
username: '用户名',
|
||||||
|
usernamePlaceholder: '请输入用户名',
|
||||||
|
usernameTip: '这是你在 Stylish Parade 上公开显示的用户名。',
|
||||||
|
role: '身份标签',
|
||||||
|
roleTip: '最多选择 2 个符合你的标签。',
|
||||||
|
},
|
||||||
|
security: {
|
||||||
|
title: '安全',
|
||||||
|
description: '管理你的登录邮箱和密码。',
|
||||||
|
email: '邮箱',
|
||||||
|
newEmail: '新邮箱地址',
|
||||||
|
newEmailPlaceholder: '请输入新邮箱',
|
||||||
|
verify: '验证',
|
||||||
|
verified: '已验证',
|
||||||
|
verifiedTip: '你的新邮箱已验证成功,可以保存。',
|
||||||
|
password: '密码',
|
||||||
|
newPassword: '新密码',
|
||||||
|
newPasswordPlaceholder: '请输入新密码',
|
||||||
|
passwordTip: '你必须满足所有密码条件才能注册。',
|
||||||
|
currentPassword: '当前密码',
|
||||||
|
currentPasswordPlaceholder: '请输入当前密码确认'
|
||||||
|
},
|
||||||
|
region: {
|
||||||
|
title: '语言与地区',
|
||||||
|
description: '设置你偏好的语言、地区和货币显示方式。',
|
||||||
|
displayLanguage: '显示语言',
|
||||||
|
selectLanguage: '请选择语言',
|
||||||
|
region: '地区',
|
||||||
|
selectRegion: '请选择地区'
|
||||||
|
},
|
||||||
|
buttons: {
|
||||||
|
cancel: '取消',
|
||||||
|
discard: '放弃',
|
||||||
|
edit: '编辑',
|
||||||
|
saveChange: '保存更改',
|
||||||
|
saving: '保存中...'
|
||||||
|
},
|
||||||
|
dialog: {
|
||||||
|
title: '检查你的新邮箱',
|
||||||
|
subtitle: '请输入发送到以下邮箱的 6 位验证码',
|
||||||
|
submit: '提交',
|
||||||
|
resendCode: '重新发送验证码',
|
||||||
|
resendCodeIn: '{time} 后可重新发送验证码'
|
||||||
|
},
|
||||||
|
messages: {
|
||||||
|
enterNewEmailFirst: '请先输入新的邮箱地址',
|
||||||
|
invalidEmail: '请输入有效的邮箱地址',
|
||||||
|
sameEmail: '请输入不同的邮箱地址',
|
||||||
|
alreadyVerified: '该邮箱已完成验证',
|
||||||
|
verificationCodeSent: '验证码已发送',
|
||||||
|
enterVerificationCode: '请输入 6 位验证码',
|
||||||
|
verificationCompleted: '邮箱验证完成',
|
||||||
|
verifyEmailBeforeSave: '请先完成新邮箱验证再保存',
|
||||||
|
settingsUpdated: '设置已更新'
|
||||||
|
},
|
||||||
|
roles: {
|
||||||
|
fashionEnthusiast: '时尚爱好者',
|
||||||
|
contentCreator: '内容创作者',
|
||||||
|
student: '学生',
|
||||||
|
retailBuyer: '零售 / 买手',
|
||||||
|
fashionDesigner: '服装设计师',
|
||||||
|
brandBusiness: '品牌 / 商业',
|
||||||
|
prCommunications: '公关与传播',
|
||||||
|
stylist: '造型师',
|
||||||
|
graphicDesigner: '平面设计师',
|
||||||
|
artist3d: '3D 艺术家',
|
||||||
|
other: '其他'
|
||||||
|
},
|
||||||
|
languages: {
|
||||||
|
english: '英文',
|
||||||
|
chinese: '中文',
|
||||||
|
},
|
||||||
|
regions: {
|
||||||
|
hongKongSar: '中国香港特别行政区',
|
||||||
|
mainlandChina: '中国大陆',
|
||||||
|
singapore: '新加坡',
|
||||||
|
unitedKingdom: '英国'
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,22 @@ const router = createRouter({
|
|||||||
path: '/brand',
|
path: '/brand',
|
||||||
name: 'brand',
|
name: 'brand',
|
||||||
component: () => import('../views/brand/index.vue')
|
component: () => import('../views/brand/index.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/brand/:id',
|
||||||
|
name: 'brandDetail',
|
||||||
|
component: () => import('../views/brandDetail/index.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/digitalItem',
|
||||||
|
name: 'digitalItem',
|
||||||
|
component: () => import('../views/digitalItem/index.vue'),
|
||||||
|
meta: { cache: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/digitalItem/:id',
|
||||||
|
name: 'digitalItemDetail',
|
||||||
|
component: () => import('../views/digitalDetail/index.vue')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/settings',
|
path: '/settings',
|
||||||
@@ -29,6 +45,26 @@ const router = createRouter({
|
|||||||
component: () => import('@/views/setting/index.vue'),
|
component: () => import('@/views/setting/index.vue'),
|
||||||
meta: { cache: true }
|
meta: { cache: true }
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/shoppingCart', // 购物车
|
||||||
|
name: 'shoppingCart',
|
||||||
|
component: () => import('@/views/shoppingCart/index.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/notifications',
|
||||||
|
name: 'notifications',
|
||||||
|
component: () => import('@/views/notifications/index.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/wardrobe',
|
||||||
|
name: 'wardrobe',
|
||||||
|
component: () => import('@/views/wardrobe/index.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path:'/account',
|
||||||
|
name:'account',
|
||||||
|
component:()=>import('@/views/account/index.vue')
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/:pathMatch(.*)',
|
path: '/:pathMatch(.*)',
|
||||||
name: '404',
|
name: '404',
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export function generateUUID(): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 备用方案:手动生成UUID v4
|
// 备用方案:手动生成UUID v4
|
||||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
||||||
const r = Math.random() * 16 | 0
|
const r = Math.random() * 16 | 0
|
||||||
const v = c === 'x' ? r : (r & 0x3 | 0x8)
|
const v = c === 'x' ? r : (r & 0x3 | 0x8)
|
||||||
return v.toString(16)
|
return v.toString(16)
|
||||||
@@ -70,39 +70,37 @@ export {
|
|||||||
|
|
||||||
/** 时间格式化-自定义格式
|
/** 时间格式化-自定义格式
|
||||||
* @param value 时间对象|时间戳|时间字符串
|
* @param value 时间对象|时间戳|时间字符串
|
||||||
* @param format 格式化字符串,默认值为 'yyyy-MM-dd HH:mm:ss'
|
* @param format 格式化字符串,默认值为 'YYYY-MM-DD HH:mm:ss'
|
||||||
* @returns 格式化后的时间字符串
|
* @returns 格式化后的时间字符串
|
||||||
*/
|
*/
|
||||||
export function FormatDate(value: Date | number | string, format: string = 'yyyy-MM-dd HH:mm:ss') {
|
export function FormatDate(value: Date | number | string, format: string = 'YYYY-MM-DD HH:mm:ss') {
|
||||||
const date = new Date(value);
|
const d = new Date(value);
|
||||||
const yyyy = String(date.getFullYear());
|
if (!d || isNaN(d.getTime())) return 'Invalid Date';
|
||||||
const yy = String(date.getFullYear()).slice(-2);
|
const pad = (n) => String(n).padStart(2, '0');
|
||||||
const MM = String(date.getMonth() + 1).padStart(2, '0');
|
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
||||||
const M = String(date.getMonth() + 1);
|
const tokens = {
|
||||||
const dd = String(date.getDate()).padStart(2, '0');
|
YYYY: d.getFullYear(),
|
||||||
const d = String(date.getDate());
|
YY: String(d.getFullYear()).slice(-2),
|
||||||
const HH = String(date.getHours()).padStart(2, '0');
|
MM: pad(d.getMonth() + 1),
|
||||||
const H = String(date.getHours());
|
M: d.getMonth() + 1,
|
||||||
const mm = String(date.getMinutes()).padStart(2, '0');
|
SM: months[d.getMonth()],
|
||||||
const m = String(date.getMinutes());
|
DD: pad(d.getDate()),
|
||||||
const ss = String(date.getSeconds()).padStart(2, '0');
|
D: d.getDate(),
|
||||||
const s = String(date.getSeconds());
|
HH: pad(d.getHours()),
|
||||||
const str = format.replaceAll('yyyy', yyyy)
|
H: d.getHours(),
|
||||||
.replaceAll('yy', yy)
|
hh: pad(d.getHours() % 12 || 12),
|
||||||
.replaceAll('MM', MM)
|
h: d.getHours() % 12 || 12,
|
||||||
.replaceAll('M', M)
|
mm: pad(d.getMinutes()),
|
||||||
.replaceAll('dd', dd)
|
m: d.getMinutes(),
|
||||||
.replaceAll('d', d)
|
ss: pad(d.getSeconds()),
|
||||||
.replaceAll('HH', HH)
|
s: d.getSeconds(),
|
||||||
.replaceAll('H', H)
|
A: d.getHours() < 12 ? 'AM' : 'PM',
|
||||||
.replaceAll('mm', mm)
|
a: d.getHours() < 12 ? 'am' : 'pm'
|
||||||
.replaceAll('m', m)
|
}
|
||||||
.replaceAll('ss', ss)
|
const reg = new RegExp(Object.keys(tokens).join('|'), 'g')
|
||||||
.replaceAll('s', s);
|
return format.replace(reg, match => tokens[match]);
|
||||||
return str;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 下载图片
|
* 下载图片
|
||||||
* @param list 图片列表
|
* @param list 图片列表
|
||||||
@@ -163,7 +161,7 @@ export function encryptPassword(password: string): string {
|
|||||||
* @param url 图片URL
|
* @param url 图片URL
|
||||||
* @returns 无
|
* @returns 无
|
||||||
*/
|
*/
|
||||||
export async function shareImageToWhatsapp (url: string){
|
export async function shareImageToWhatsapp(url: string) {
|
||||||
// 把图片 URL 转为 Blob
|
// 把图片 URL 转为 Blob
|
||||||
const blob = await fetch(url).then((res) => res.blob())
|
const blob = await fetch(url).then((res) => res.blob())
|
||||||
|
|
||||||
@@ -195,3 +193,21 @@ export function CountDown(time: number) {
|
|||||||
const ss = String(time % 60).padStart(2, '0');
|
const ss = String(time % 60).padStart(2, '0');
|
||||||
return `${mm}:${ss}`;
|
return `${mm}:${ss}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 字节转换为可读格式
|
||||||
|
* @param {number} bytes - 字节数
|
||||||
|
* @param {number} options - 选项对象
|
||||||
|
* @param {number} options.decimals - 保留小数位数,默认2位
|
||||||
|
* @param {boolean} options.unitBig - 是否使用大写单位,默认false
|
||||||
|
* @returns {string} 格式化后的字符串
|
||||||
|
*/
|
||||||
|
export function FormatBytes(bytes, options: { decimals?: number, unitBig?: boolean } = {}) {
|
||||||
|
const { decimals = 2, unitBig = false } = options;
|
||||||
|
if (!bytes || isNaN(bytes)) return unitBig ? '0 B' : '0 b';
|
||||||
|
const k = 1024;
|
||||||
|
const sizes = unitBig ? ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] : ['b', 'kb', 'mb', 'gb', 'tb', 'pb', 'eb', 'zb', 'yb'];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
const value = bytes / Math.pow(k, i);
|
||||||
|
return `${Number(value.toFixed(decimals))} ${sizes[i]}`;
|
||||||
|
}
|
||||||
505
src/views/account/index.vue
Normal file
@@ -0,0 +1,505 @@
|
|||||||
|
<template>
|
||||||
|
<div class="account-container">
|
||||||
|
<img src="@/assets/images/account/account-bg.png" alt="" class="banner" />
|
||||||
|
|
||||||
|
<div class="account-main">
|
||||||
|
<aside class="designer-panel">
|
||||||
|
<div class="designer-avatar">
|
||||||
|
<img :src="designerPortrait" alt="Lian Su" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="designer-content">
|
||||||
|
<section class="designer-heading">
|
||||||
|
<div class="designer-name">Lian Su</div>
|
||||||
|
<h1 class="designer-title">Roaming Clouds</h1>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="designer-section">
|
||||||
|
<h2 class="section-title">Contact</h2>
|
||||||
|
<div class="contact-list">
|
||||||
|
<div class="contact-item">
|
||||||
|
<SvgIcon name="brand-email" size="24" />
|
||||||
|
<span>lian.su.studio@mail.com</span>
|
||||||
|
</div>
|
||||||
|
<div class="contact-item">
|
||||||
|
<SvgIcon name="brand-call" size="24" />
|
||||||
|
<span>+86 139 4829 7710</span>
|
||||||
|
</div>
|
||||||
|
<div class="contact-item">
|
||||||
|
<SvgIcon name="brand-link" size="24" />
|
||||||
|
<span>746312432</span>
|
||||||
|
</div>
|
||||||
|
<div class="contact-item">
|
||||||
|
<SvgIcon name="brand-link" size="24" />
|
||||||
|
<span>https://urieworweoo.com</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="designer-section">
|
||||||
|
<h2 class="section-title">About</h2>
|
||||||
|
<p class="designer-about">
|
||||||
|
Lian Su's work weaves understated ethnic influences into contemporary minimalism.
|
||||||
|
She explores materials and silhouettes that bridge heritage and modern sensibilities.
|
||||||
|
Her designs reflect a quiet dialogue between cultural memory and forward-looking innovation.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<section class="items-section">
|
||||||
|
<header class="items-header">
|
||||||
|
<h2 class="items-title">Items</h2>
|
||||||
|
<div class="items-tabs" role="tablist" aria-label="Item gender filters">
|
||||||
|
<button
|
||||||
|
v-for="tab in tabs"
|
||||||
|
:key="tab"
|
||||||
|
type="button"
|
||||||
|
class="items-tab"
|
||||||
|
:class="{ active: activeTab === tab }"
|
||||||
|
:aria-selected="activeTab === tab"
|
||||||
|
role="tab"
|
||||||
|
@click="activeTab = tab"
|
||||||
|
>
|
||||||
|
{{ tab }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="items-grid">
|
||||||
|
<article v-for="item in visibleItems" :key="item.id" class="product-card">
|
||||||
|
<div class="product-image">
|
||||||
|
<img
|
||||||
|
:src="item.url"
|
||||||
|
:alt="item.title"
|
||||||
|
:style="{ transform: `translateY(${item.imageOffset})` }"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="product-info">
|
||||||
|
<div class="product-copy">
|
||||||
|
<h3 class="product-name">{{ item.title }}</h3>
|
||||||
|
<div class="product-price">{{ item.price }}</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="add-button"
|
||||||
|
:aria-label="`Add ${item.title} to cart`"
|
||||||
|
@click="addShopping(item)"
|
||||||
|
>
|
||||||
|
<SvgIcon name="add" size="24" color="#232323" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, shallowRef } from 'vue'
|
||||||
|
import myEvent from '@/utils/myEvent'
|
||||||
|
import designerPortrait from '@/assets/images/account/designer-lian-su.png'
|
||||||
|
import item01 from '@/assets/images/account/item-01.png'
|
||||||
|
import item02 from '@/assets/images/account/item-02.png'
|
||||||
|
import item03 from '@/assets/images/account/item-03.png'
|
||||||
|
import item04 from '@/assets/images/account/item-04.png'
|
||||||
|
import item05 from '@/assets/images/account/item-05.png'
|
||||||
|
import item06 from '@/assets/images/account/item-06.png'
|
||||||
|
import item07 from '@/assets/images/account/item-07.png'
|
||||||
|
import item08 from '@/assets/images/account/item-08.png'
|
||||||
|
import item09 from '@/assets/images/account/item-09.png'
|
||||||
|
|
||||||
|
type Gender = 'Male' | 'Female'
|
||||||
|
type Tab = 'All' | Gender
|
||||||
|
|
||||||
|
interface AccountItem {
|
||||||
|
id: number
|
||||||
|
title: string
|
||||||
|
price: string
|
||||||
|
url: string
|
||||||
|
gender: Gender
|
||||||
|
imageOffset: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const tabs: Tab[] = ['All', 'Male', 'Female']
|
||||||
|
const activeTab = shallowRef<Tab>('All')
|
||||||
|
|
||||||
|
const items: AccountItem[] = [
|
||||||
|
{ id: 1, title: 'Item Name', price: '$430', url: item01, gender: 'Female', imageOffset: '0rem' },
|
||||||
|
{ id: 2, title: 'Item Name', price: '$392', url: item02, gender: 'Female', imageOffset: '0rem' },
|
||||||
|
{ id: 3, title: 'Item Name', price: '$211', url: item03, gender: 'Male', imageOffset: '0rem' },
|
||||||
|
{ id: 4, title: 'Item Name', price: '$187', url: item04, gender: 'Female', imageOffset: '0rem' },
|
||||||
|
{ id: 5, title: 'Item Name', price: '$325', url: item05, gender: 'Male', imageOffset: '-1.4rem' },
|
||||||
|
{ id: 6, title: 'Item Name', price: '$458', url: item06, gender: 'Female', imageOffset: '-0.4rem' },
|
||||||
|
{ id: 7, title: 'Item Name', price: '$192', url: item07, gender: 'Male', imageOffset: '-3.6rem' },
|
||||||
|
{ id: 8, title: 'Item Name', price: '$93', url: item08, gender: 'Female', imageOffset: '0rem' },
|
||||||
|
{ id: 9, title: 'Item Name', price: '$198', url: item09, gender: 'Male', imageOffset: '-3.8rem' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const visibleItems = computed(() => {
|
||||||
|
if (activeTab.value === 'All') return items
|
||||||
|
return items.filter((item) => item.gender === activeTab.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
const addShopping = (item: AccountItem) => {
|
||||||
|
myEvent.emit('addShopping', item)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.account-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
overflow-y: auto;
|
||||||
|
background-color: #ffffff;
|
||||||
|
|
||||||
|
.banner {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 27.6rem;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-main {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin: 0 9rem;
|
||||||
|
min-height: 170.3rem;
|
||||||
|
border-top: 0.5px solid #585858;
|
||||||
|
}
|
||||||
|
|
||||||
|
.designer-panel {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
width: 29.7rem;
|
||||||
|
height: var(--app-view-height);
|
||||||
|
padding-top: 4rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
flex: 0 0 29.7rem;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.designer-avatar {
|
||||||
|
width: 20rem;
|
||||||
|
height: 20rem;
|
||||||
|
margin-left: 5.5rem;
|
||||||
|
border: 0.1rem solid #d2d2d7;
|
||||||
|
|
||||||
|
> img {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.designer-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6rem;
|
||||||
|
width: 24.6rem;
|
||||||
|
margin-top: 4rem;
|
||||||
|
margin-left: 3.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.designer-heading,
|
||||||
|
.designer-section {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.designer-name {
|
||||||
|
margin-bottom: 0.8rem;
|
||||||
|
font-family: KaiseiOpti-Medium;
|
||||||
|
font-size: 1.8rem;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1;
|
||||||
|
color: #232323;
|
||||||
|
}
|
||||||
|
|
||||||
|
.designer-title,
|
||||||
|
.section-title {
|
||||||
|
margin: 0;
|
||||||
|
font-family: KaiseiOpti-Bold;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #121212;
|
||||||
|
}
|
||||||
|
|
||||||
|
.designer-title {
|
||||||
|
width: 23.1rem;
|
||||||
|
font-size: 3.4rem;
|
||||||
|
line-height: 3.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
font-size: 3.4rem;
|
||||||
|
line-height: 3.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2rem;
|
||||||
|
min-width: 0;
|
||||||
|
font-family: KaiseiOpti-Regular;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 2.4rem;
|
||||||
|
color: #585858;
|
||||||
|
|
||||||
|
> :first-child {
|
||||||
|
width: 2.4rem;
|
||||||
|
height: 2.4rem;
|
||||||
|
flex: 0 0 2.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
> span {
|
||||||
|
min-width: 0;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.designer-about {
|
||||||
|
width: 22rem;
|
||||||
|
margin: 0;
|
||||||
|
font-family: KaiseiOpti-Regular;
|
||||||
|
font-size: 1.6rem;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 2.3rem;
|
||||||
|
color: #585858;
|
||||||
|
}
|
||||||
|
|
||||||
|
.items-section {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 170.3rem;
|
||||||
|
border-left: 0.5px solid #585858;
|
||||||
|
border-right: 0.5px solid #585858;
|
||||||
|
}
|
||||||
|
|
||||||
|
.items-header {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 2;
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.items-title {
|
||||||
|
margin: 0;
|
||||||
|
padding: 4rem 0 3.6rem 1.2rem;
|
||||||
|
font-family: KaiseiOpti-Bold;
|
||||||
|
font-size: 3.6rem;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 6rem;
|
||||||
|
color: #121212;
|
||||||
|
}
|
||||||
|
|
||||||
|
.items-tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 2rem;
|
||||||
|
padding: 0 1.2rem;
|
||||||
|
margin-bottom: 5.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.items-tab {
|
||||||
|
position: relative;
|
||||||
|
min-width: 6rem;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
font-family: KaiseiOpti-Regular;
|
||||||
|
font-size: 1.988rem;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 2.6rem;
|
||||||
|
text-align: center;
|
||||||
|
color: #7b7b7b;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
bottom: -0.1rem;
|
||||||
|
display: none;
|
||||||
|
width: 100%;
|
||||||
|
border-bottom: 0.1rem solid #232323;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
font-family: KaiseiOpti-Bold;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #232323;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.items-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
align-content: start;
|
||||||
|
border-top: 0.5px solid #585858;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-card {
|
||||||
|
position: relative;
|
||||||
|
min-height: 44.7rem;
|
||||||
|
padding: 1.2rem;
|
||||||
|
|
||||||
|
&::before,
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 100%;
|
||||||
|
border-right: 0.5px solid #585858;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
border-bottom: 0.5px solid #585858;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(3n)::before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-image {
|
||||||
|
height: 37.5rem;
|
||||||
|
margin-bottom: 0.8rem;
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: #f7f7f7;
|
||||||
|
|
||||||
|
> img {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-copy {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-name {
|
||||||
|
margin: 0;
|
||||||
|
font-family: KaiseiOpti-Regular;
|
||||||
|
font-size: 1.6rem;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 2.3rem;
|
||||||
|
color: #232323;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-price {
|
||||||
|
font-family: KaiseiOpti-Regular;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 2.3rem;
|
||||||
|
color: #585858;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-button {
|
||||||
|
width: 2.4rem;
|
||||||
|
height: 2.4rem;
|
||||||
|
padding: 0;
|
||||||
|
margin-top: 1rem;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
flex: 0 0 2.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.account-main {
|
||||||
|
display: block;
|
||||||
|
margin: 0 2.4rem;
|
||||||
|
min-height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.designer-panel {
|
||||||
|
position: static;
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
padding: 4rem 0;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.designer-avatar {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.designer-content {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.items-section {
|
||||||
|
min-height: auto;
|
||||||
|
border-left: none;
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.items-header {
|
||||||
|
position: static;
|
||||||
|
}
|
||||||
|
|
||||||
|
.items-grid {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-card:nth-child(3n)::before {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-card:nth-child(2n)::before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.account-main {
|
||||||
|
margin: 0 1.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.items-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-card::before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
127
src/views/brand/brand-item.vue
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, onUnmounted, reactive, toRefs } from "vue";
|
||||||
|
const props = defineProps({
|
||||||
|
item:{
|
||||||
|
type:Object,
|
||||||
|
default:()=>{},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const emit = defineEmits([
|
||||||
|
'viewProfile',
|
||||||
|
])
|
||||||
|
const viewProfile = (item) => {
|
||||||
|
emit('viewProfile', item)
|
||||||
|
}
|
||||||
|
let data = reactive({
|
||||||
|
})
|
||||||
|
onMounted(()=>{
|
||||||
|
})
|
||||||
|
onUnmounted(()=>{
|
||||||
|
})
|
||||||
|
defineExpose({})
|
||||||
|
const {} = toRefs(data);
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="item">
|
||||||
|
<div class="left">
|
||||||
|
<div class="portrait">
|
||||||
|
<img :src="item.portrait" alt="">
|
||||||
|
</div>
|
||||||
|
<div class="info">
|
||||||
|
<div class="name">{{ item.name }}</div>
|
||||||
|
<div class="collection">
|
||||||
|
{{ item.collectionsName }} |
|
||||||
|
{{ item?.collections?.length || 0 }} Collections
|
||||||
|
</div>
|
||||||
|
<div class="view-profile" @click="viewProfile(item)">View Profile</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="right">
|
||||||
|
<div class="img-list">
|
||||||
|
<div class="img-item" v-for="itemImg in item?.collections?.slice(0,5)" :key="item.id">
|
||||||
|
<img :src="itemImg" alt="">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="more">
|
||||||
|
<div class="icon" v-show="item?.collections?.length > 5">
|
||||||
|
<svgIcon name="brand-more" size="24" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.item{
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
height: 14rem;
|
||||||
|
justify-content: space-between;
|
||||||
|
border-bottom: 0.5px solid #C4C4C4;
|
||||||
|
> .left{
|
||||||
|
display: flex;
|
||||||
|
.portrait{
|
||||||
|
width: 8rem;
|
||||||
|
height: 8rem;
|
||||||
|
margin-right: 2.4rem;
|
||||||
|
> img{
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.info{
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
> .name{
|
||||||
|
font-family: "KaiseiOpti-Bold";
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 2rem;
|
||||||
|
line-height: 100%;
|
||||||
|
}
|
||||||
|
> .collection{
|
||||||
|
color: #7B7B7B;
|
||||||
|
font-family: "KaiseiOpti-Regular";
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
line-height: 2.3rem;
|
||||||
|
margin-top: .4rem;
|
||||||
|
}
|
||||||
|
> .view-profile{
|
||||||
|
margin-top: 2.4rem;
|
||||||
|
border-bottom: 2px solid #585858;
|
||||||
|
background: #F9F9F9;
|
||||||
|
font-family: "KaiseiOpti-Regular";
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
letter-spacing: -0.4px;
|
||||||
|
line-height: 3.4rem;
|
||||||
|
padding: 0 2.55rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
> .right{
|
||||||
|
display: flex;
|
||||||
|
> .img-list{
|
||||||
|
display: flex;
|
||||||
|
gap: 3.2rem;
|
||||||
|
> .img-item{
|
||||||
|
width: 9.2rem;
|
||||||
|
height: 12rem;
|
||||||
|
img{
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
> .more{
|
||||||
|
width: 8rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,14 +1,108 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, onUnmounted, reactive, toRefs } from "vue";
|
import { ref, onMounted, onUnmounted, reactive, toRefs } from "vue";
|
||||||
import CommodityList from "./commodity-list.vue";
|
import { useRouter } from "vue-router";
|
||||||
import MerchantInfo from "./merchant-info.vue";
|
import myEvent from '@/utils/myEvent'
|
||||||
|
import scListNull from '@/views/shoppingCart/sc-list-null.vue'
|
||||||
|
import brandItem from '@/views/brand/brand-item.vue'
|
||||||
|
|
||||||
|
import img from '@/assets/images/collectionStory/Rectangle.png'
|
||||||
//const props = defineProps({
|
//const props = defineProps({
|
||||||
//})
|
//})
|
||||||
//const emit = defineEmits([
|
//const emit = defineEmits([
|
||||||
//])
|
//])
|
||||||
|
const router = useRouter()
|
||||||
let data = reactive({
|
let data = reactive({
|
||||||
})
|
})
|
||||||
const addShopping = (item) => {}
|
|
||||||
|
const searchBrand = ref('')
|
||||||
|
const merchantList = ref([
|
||||||
|
])
|
||||||
|
const getMerchantData = reactive({
|
||||||
|
pageSize: 10,
|
||||||
|
pageNum: 1,
|
||||||
|
isShowMark:false,
|
||||||
|
isNoData:false,
|
||||||
|
})
|
||||||
|
const list = ref([
|
||||||
|
' 1',
|
||||||
|
'Brand 2',
|
||||||
|
'Brand 3',
|
||||||
|
'1213123 4',
|
||||||
|
'Brand 4',
|
||||||
|
'2222 4',
|
||||||
|
'B23rand 4',
|
||||||
|
'Bran112222d 4',
|
||||||
|
' 4',
|
||||||
|
])
|
||||||
|
|
||||||
|
let changeSearchBrandTime = null
|
||||||
|
const changeSearchBrand = () => {
|
||||||
|
clearTimeout(changeSearchBrandTime)
|
||||||
|
changeSearchBrandTime = setTimeout(()=>{
|
||||||
|
getMerchantData.pageNum = 1
|
||||||
|
merchantList.value = []
|
||||||
|
getMerchantData.isShowMark = false
|
||||||
|
getMerchantData.isNoData = false
|
||||||
|
},300)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getBrandList = async () => {
|
||||||
|
if(getMerchantData.isShowMark && !getMerchantData.isNoData)return
|
||||||
|
getMerchantData.isShowMark = true
|
||||||
|
let value = {
|
||||||
|
pageSize: getMerchantData.pageSize,
|
||||||
|
pageNum: getMerchantData.pageNum,
|
||||||
|
status: 1,
|
||||||
|
}
|
||||||
|
setTimeout(()=>{
|
||||||
|
if(merchantList.value.length >= 5){
|
||||||
|
getMerchantData.isNoData = true
|
||||||
|
merchantList.value = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
getMerchantData.pageNum += 1
|
||||||
|
merchantList.value.push({
|
||||||
|
name:'Roaming Clouds',
|
||||||
|
portrait: img,
|
||||||
|
collectionsName:'by Lian Su ',
|
||||||
|
collections:[
|
||||||
|
img,img,img,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
getMerchantData.isShowMark = false
|
||||||
|
},1000)
|
||||||
|
// await getPublishList(value).then((res)=>{
|
||||||
|
// if(res.content.length == 0)getMerchantData.isNoData = true
|
||||||
|
// getMerchantData.pageNum += 1
|
||||||
|
// list.value.push(...res.content)
|
||||||
|
// })
|
||||||
|
}
|
||||||
|
const vObserve = {
|
||||||
|
mounted (el,binding) {
|
||||||
|
getMerchantData.isShowMark = false
|
||||||
|
getMerchantData.isNoData = false
|
||||||
|
new IntersectionObserver(
|
||||||
|
(entries, observer) => {
|
||||||
|
// 如果不是相交,则直接返回
|
||||||
|
// console.log(entries[0]);
|
||||||
|
if (!entries[0].intersectionRatio) return;
|
||||||
|
getMerchantData.pageNum += 1
|
||||||
|
binding.value()
|
||||||
|
},
|
||||||
|
// { root:worksPage }
|
||||||
|
).observe(el);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteHistory = (item) => {
|
||||||
|
list.value = list.value.filter((i) => i != item)
|
||||||
|
}
|
||||||
|
const viewProfile = (item) => {
|
||||||
|
router.push({
|
||||||
|
path:'/brand/1',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(()=>{
|
onMounted(()=>{
|
||||||
})
|
})
|
||||||
onUnmounted(()=>{
|
onUnmounted(()=>{
|
||||||
@@ -18,15 +112,52 @@ const {} = toRefs(data);
|
|||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div class="brand">
|
<div class="brand">
|
||||||
<div class="header-img">
|
<div class="header-img" :class="{'active': searchBrand.length > 0}">
|
||||||
<img src="@/assets/images/brand/brandBg.png" alt="">
|
<img src="@/assets/images/brand/brandBg.png" alt="">
|
||||||
|
<div class="text-box">
|
||||||
|
<div class="title">Brand</div>
|
||||||
|
<span>Every brand, every story — discover who's behind the collections.</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<div class="merchant-info">
|
<div class="input">
|
||||||
<MerchantInfo></MerchantInfo>
|
<input type="text" v-model="searchBrand" @input="changeSearchBrand" placeholder="Search brand">
|
||||||
|
<div class="icon">
|
||||||
|
<SvgIcon name="brand-search" size="32" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="commodity-list">
|
<div class="merchantList" v-if="searchBrand.length > 0">
|
||||||
<CommodityList @addShopping="addShopping"></CommodityList>
|
<brand-item v-for="item in merchantList" :key="item.name" :item="item" @viewProfile="viewProfile"></brand-item>
|
||||||
|
<div class="end" v-show="!getMerchantData.isNoData && !getMerchantData.isShowMark">- The End-</div>
|
||||||
|
<div v-show="!getMerchantData.isNoData" class="material_content_list_loding">
|
||||||
|
<span class="page_loading" v-show="!getMerchantData.isShowMark" v-observe="getBrandList"></span>
|
||||||
|
<img v-if="getMerchantData.isShowMark" src="@/assets/images/brand/brandLoading.gif" alt="">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="merchantListNull" v-if="getMerchantData.isNoData && searchBrand.length > 0">
|
||||||
|
<sc-list-null
|
||||||
|
nullImage="brand"
|
||||||
|
:showButton="false"
|
||||||
|
title="Brand No Found"
|
||||||
|
tip="Try using another keywords."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="null-input" v-if="searchBrand.length == 0">
|
||||||
|
<div class="title">
|
||||||
|
<div class="icon">
|
||||||
|
<SvgIcon name="brand-time" size="20" />
|
||||||
|
</div>
|
||||||
|
<span>Searching History</span>
|
||||||
|
</div>
|
||||||
|
<div class="history">
|
||||||
|
<div v-for="item in list" :key="item" @click.stop="searchBrand = item" class="item">
|
||||||
|
<span>{{item}}</span>
|
||||||
|
<div class="icon" @click.stop="deleteHistory(item)">
|
||||||
|
<SvgIcon name="brand-delete" size="18" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Footer></Footer>
|
<Footer></Footer>
|
||||||
@@ -38,34 +169,151 @@ const {} = toRefs(data);
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
.header-img{
|
.header-img{
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
height: 34.4rem;
|
||||||
|
transition: all .3s;
|
||||||
|
&.active{
|
||||||
|
height: 14.7rem;
|
||||||
|
}
|
||||||
>img{
|
>img{
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
> .text-box{
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
text-align: center;
|
||||||
|
> .title{
|
||||||
|
font-family: 'KaiseiOpti-Bold';
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 4rem;
|
||||||
|
line-height: 2.3rem;
|
||||||
|
letter-spacing: 0%;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
> span{
|
||||||
|
display: block;
|
||||||
|
margin-top: 2.4rem;
|
||||||
|
font-family: 'KaiseiOpti-Regular';
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 1.6rem;
|
||||||
|
line-height: 140%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.content{
|
>.content{
|
||||||
display: flex;
|
display: flex;
|
||||||
height: auto;
|
height: auto;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
.merchant-info{
|
flex-direction: column;
|
||||||
width: 40rem;
|
align-items: center;
|
||||||
padding-left: 12.7rem;
|
flex: 1;
|
||||||
padding-right: 2.7rem;
|
overflow: hidden;
|
||||||
height: var(--app-view-height);
|
> .input{
|
||||||
overflow-y: auto;
|
width: 66.6rem;
|
||||||
position: sticky;
|
display: flex;
|
||||||
top: 0;
|
border-bottom: 2px solid #232323;
|
||||||
&::-webkit-scrollbar{
|
padding: 1.4rem 1.4rem 1.4rem 2.4rem;
|
||||||
width: 0;
|
background-color: #f9f9f9;
|
||||||
height: 0;
|
display: flex;
|
||||||
|
margin-top: 8rem;
|
||||||
|
> input{
|
||||||
|
width: 57.2rem;
|
||||||
|
line-height: 3.2rem;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
font-family: 'KaiseiOpti-Regular';
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 1.6rem;
|
||||||
|
line-height: 100%;
|
||||||
|
margin-right: 2.4rem;
|
||||||
|
background-color: transparent;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
> .icon{
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.commodity-list{
|
> .null-input{
|
||||||
|
margin-top: 8rem;
|
||||||
|
.title{
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
.icon{
|
||||||
|
margin-right: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.history{
|
||||||
|
margin-top: 4rem;
|
||||||
|
width: 59rem;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
column-gap: 3.2rem;
|
||||||
|
row-gap: 1.2rem;
|
||||||
|
.item{
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
> span{
|
||||||
|
font-family: 'KaiseiOpti-Regular';
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
line-height: 100%;
|
||||||
|
margin-right: .4rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
> .merchantList{
|
||||||
|
width: 121.8rem;
|
||||||
|
margin-top: 6rem;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
border-left: 0.5px solid #585858;
|
overflow-y: auto;
|
||||||
border-right: 0.5px solid #585858;
|
gap: 3.2rem;
|
||||||
margin-right: 9rem;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
::-webkit-scrollbar{
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.end{
|
||||||
|
font-family: 'KaiseiOpti-Regular';
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 140%;
|
||||||
|
height: 7rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
> .material_content_list_loding{
|
||||||
|
width: 100%;
|
||||||
|
height: 5rem;
|
||||||
|
aspect-ratio: 1/1;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
justify-content: center;
|
||||||
|
> .page_loading{
|
||||||
|
width: 5rem;
|
||||||
|
height: 5rem;
|
||||||
|
}
|
||||||
|
> img{
|
||||||
|
width: 5rem;
|
||||||
|
height: 5rem;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
> .merchantListNull{
|
||||||
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,11 +37,7 @@ const list = ref([
|
|||||||
url: img,
|
url: img,
|
||||||
title: "Windswept Burden",
|
title: "Windswept Burden",
|
||||||
price: "$100.00",
|
price: "$100.00",
|
||||||
},{
|
}
|
||||||
url: img,
|
|
||||||
title: "Windswept Burden",
|
|
||||||
price: "$100.00",
|
|
||||||
},
|
|
||||||
])
|
])
|
||||||
const type = ref('All')
|
const type = ref('All')
|
||||||
const addShopping = (item) => {
|
const addShopping = (item) => {
|
||||||
@@ -128,41 +124,26 @@ const {} = toRefs(data);
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.list{
|
.list{
|
||||||
border-top: 0.5px solid #585858;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
// display: grid;
|
||||||
|
// align-content: start;
|
||||||
|
// grid-template-columns: repeat(3, 1fr);
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
display: grid;
|
display: grid;
|
||||||
align-content: start;
|
align-content: start;
|
||||||
grid-template-columns: repeat(3, 1fr);
|
grid-template-columns: repeat(auto-fit, minmax(min(100%, 28rem), 1fr));
|
||||||
overflow-y: auto;
|
border-top: 0.5px solid #585858;
|
||||||
|
padding: .5px 0 0 .5px;
|
||||||
/* 垂直线(右边框) */
|
/* 垂直线(右边框) */
|
||||||
.item{
|
.item{
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 1.2rem;
|
padding: 1.2rem;
|
||||||
}
|
|
||||||
.item::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
top: 0;
|
|
||||||
height: 100%;
|
|
||||||
border-right: 0.5px solid #585858;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 水平线(下边框) */
|
|
||||||
.item::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
border-bottom: 0.5px solid #585858;
|
border-bottom: 0.5px solid #585858;
|
||||||
z-index: 1;
|
border-right: 0.5px solid #585858;
|
||||||
}
|
margin-right: -1px;
|
||||||
/* 移除最后一列的右边框 */
|
margin-bottom: -1px;
|
||||||
.item:nth-child(3n)::before {
|
|
||||||
display: none;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
80
src/views/brandDetail/index.vue
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, onUnmounted, reactive, toRefs } from "vue";
|
||||||
|
import CommodityList from "./commodity-list.vue";
|
||||||
|
import MerchantInfo from "./merchant-info.vue";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
import myEvent from '@/utils/myEvent'
|
||||||
|
//const props = defineProps({
|
||||||
|
//})
|
||||||
|
//const emit = defineEmits([
|
||||||
|
//])
|
||||||
|
const router = useRouter()
|
||||||
|
let data = reactive({
|
||||||
|
})
|
||||||
|
const addShopping = (item) => {
|
||||||
|
myEvent.emit('addShopping', item)
|
||||||
|
}
|
||||||
|
const openDetail = (item) => {
|
||||||
|
router.push({name: 'digitalDetail', params: {id: item.id}})
|
||||||
|
}
|
||||||
|
onMounted(()=>{
|
||||||
|
})
|
||||||
|
onUnmounted(()=>{
|
||||||
|
})
|
||||||
|
defineExpose({})
|
||||||
|
const {} = toRefs(data);
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="brand">
|
||||||
|
<div class="header-img">
|
||||||
|
<img src="@/assets/images/brand/brandDetailBg.png" alt="">
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<div class="merchant-info">
|
||||||
|
<MerchantInfo></MerchantInfo>
|
||||||
|
</div>
|
||||||
|
<div class="commodity-list">
|
||||||
|
<CommodityList @addShopping="addShopping" @openDetail="openDetail"></CommodityList>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Footer></Footer>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.brand{
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
overflow-y: auto;
|
||||||
|
.header-img{
|
||||||
|
width: 100%;
|
||||||
|
>img{
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.content{
|
||||||
|
display: flex;
|
||||||
|
height: auto;
|
||||||
|
align-items: flex-start;
|
||||||
|
.merchant-info{
|
||||||
|
width: 40rem;
|
||||||
|
padding-left: 12.7rem;
|
||||||
|
padding-right: 2.7rem;
|
||||||
|
height: var(--app-view-height);
|
||||||
|
overflow-y: auto;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
&::-webkit-scrollbar{
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.commodity-list{
|
||||||
|
flex: 1;
|
||||||
|
border-left: 0.5px solid #585858;
|
||||||
|
border-right: 0.5px solid #585858;
|
||||||
|
margin-right: 9rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -4,11 +4,13 @@ import img from "@/assets/images/collectionStory/Rectangle.png";
|
|||||||
import coreConcept from "./coreConcept.vue";
|
import coreConcept from "./coreConcept.vue";
|
||||||
import inspiration from "./inspiration.vue";
|
import inspiration from "./inspiration.vue";
|
||||||
import feelingWithAiDA from "./feelingWithAiDA.vue";
|
import feelingWithAiDA from "./feelingWithAiDA.vue";
|
||||||
|
import joinUs from "./join-us.vue";
|
||||||
import CommodityItem from "@/components/CommodityItem.vue";
|
import CommodityItem from "@/components/CommodityItem.vue";
|
||||||
//const props = defineProps({
|
//const props = defineProps({
|
||||||
//})
|
//})
|
||||||
//const emit = defineEmits([
|
const emit = defineEmits([
|
||||||
//])
|
'addShopping'
|
||||||
|
])
|
||||||
let data = reactive({
|
let data = reactive({
|
||||||
})
|
})
|
||||||
const list = ref([
|
const list = ref([
|
||||||
@@ -27,8 +29,12 @@ const list = ref([
|
|||||||
},
|
},
|
||||||
])
|
])
|
||||||
const addShopping = (item) => {
|
const addShopping = (item) => {
|
||||||
console.log(item);
|
emit('addShopping', item)
|
||||||
}
|
}
|
||||||
|
const openCodeCreate = () => {
|
||||||
|
window.open('https://code-create.com.hk/', '_blank')
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(()=>{
|
onMounted(()=>{
|
||||||
})
|
})
|
||||||
onUnmounted(()=>{
|
onUnmounted(()=>{
|
||||||
@@ -40,21 +46,22 @@ const {} = toRefs(data);
|
|||||||
<div class="detail">
|
<div class="detail">
|
||||||
<div class="left">
|
<div class="left">
|
||||||
<div class="personal">
|
<div class="personal">
|
||||||
<img :src="img" alt="">
|
<img src="@/assets/images/collectionStory/code-create.png" alt="">
|
||||||
<div class="name">
|
<div class="name">
|
||||||
<span>Lian Su</span>
|
<span>Code-Create</span>
|
||||||
<div class="icon">
|
<div class="icon" @click="openCodeCreate">
|
||||||
<SvgIcon name="share" size="24" />
|
<SvgIcon name="share" size="24" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="center">
|
<div class="center">
|
||||||
<coreConcept ></coreConcept>
|
<joinUs></joinUs>
|
||||||
|
<!-- <coreConcept ></coreConcept>
|
||||||
<div class="line"></div>
|
<div class="line"></div>
|
||||||
<inspiration ></inspiration>
|
<inspiration ></inspiration>
|
||||||
<div class="line"></div>
|
<div class="line"></div>
|
||||||
<feelingWithAiDA ></feelingWithAiDA>
|
<feelingWithAiDA ></feelingWithAiDA> -->
|
||||||
</div>
|
</div>
|
||||||
<div class="right">
|
<div class="right">
|
||||||
<div class="item" v-for="item in list" :key="item.url">
|
<div class="item" v-for="item in list" :key="item.url">
|
||||||
@@ -66,10 +73,11 @@ const {} = toRefs(data);
|
|||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
.detail{
|
.detail{
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: auto;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
height: calc(100vh - var(--header-height) - var(--footer-height));
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
|
overflow: hidden;
|
||||||
> div{
|
> div{
|
||||||
// height: 100%;
|
// height: 100%;
|
||||||
}
|
}
|
||||||
@@ -92,6 +100,9 @@ const {} = toRefs(data);
|
|||||||
display: flex;
|
display: flex;
|
||||||
gap: .4rem;
|
gap: .4rem;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
> .icon{
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
> span{
|
> span{
|
||||||
font-family: 'KaiseiOpti-Bold';
|
font-family: 'KaiseiOpti-Bold';
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
@@ -109,8 +120,9 @@ const {} = toRefs(data);
|
|||||||
border-right: 0.5px solid #585858;
|
border-right: 0.5px solid #585858;
|
||||||
// overflow-y: auto;
|
// overflow-y: auto;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
// height: 100%;
|
height: 100%;
|
||||||
height: auto;
|
// height: auto;
|
||||||
|
position: relative;
|
||||||
.line{
|
.line{
|
||||||
border: 0.5px solid #58585899;
|
border: 0.5px solid #58585899;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -122,14 +134,15 @@ const {} = toRefs(data);
|
|||||||
> .right{
|
> .right{
|
||||||
width: 25.4rem;
|
width: 25.4rem;
|
||||||
padding-top: 6rem;
|
padding-top: 6rem;
|
||||||
position: sticky;
|
// position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
height: calc(100vh - var(--header-height));
|
// height: calc(100vh - var(--header-height));
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 4rem;
|
gap: 4rem;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
height: 100%;
|
||||||
&::-webkit-scrollbar{
|
&::-webkit-scrollbar{
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|||||||
59
src/views/collectionStory/detail/join-us.vue
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, onUnmounted, reactive, toRefs } from "vue";
|
||||||
|
//const props = defineProps({
|
||||||
|
//})
|
||||||
|
//const emit = defineEmits([
|
||||||
|
//])
|
||||||
|
let data = reactive({
|
||||||
|
})
|
||||||
|
onMounted(()=>{
|
||||||
|
})
|
||||||
|
onUnmounted(()=>{
|
||||||
|
})
|
||||||
|
defineExpose({})
|
||||||
|
const {} = toRefs(data);
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="joinUs">
|
||||||
|
<div class="title">Join Our Designer Community</div>
|
||||||
|
<div class="info">
|
||||||
|
<div>
|
||||||
|
Join our community of visionaries and publish your collection story.
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
We are currently seeking collections that deeply integrate the AiDA creative workflow, specifically those that resonate through powerful core concepts and evocative inspiration.
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
<div>
|
||||||
|
This architecture is designed to elevate your exposure through profound "propositional expression," ensuring that soulful, story-driven designs achieve higher market premiums and superior sales conversion.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.joinUs{
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
padding: 6rem 0;
|
||||||
|
> .title{
|
||||||
|
padding: 0 2.4rem;
|
||||||
|
font-family: "KaiseiOpti-Bold";
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 3rem;
|
||||||
|
line-height: 100%;
|
||||||
|
margin-bottom: 6rem;
|
||||||
|
}
|
||||||
|
> .info{
|
||||||
|
width: 70.7rem;
|
||||||
|
margin: 0 auto;
|
||||||
|
margin-bottom: 4rem;
|
||||||
|
> div{
|
||||||
|
font-family: "KaiseiOpti-Regular";
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 1.6rem;
|
||||||
|
line-height: 2.3rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,13 +1,16 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, onUnmounted, reactive, toRefs } from "vue";
|
import { ref, onMounted, onUnmounted, reactive, toRefs } from "vue";
|
||||||
import Detail from "./detail/index.vue";
|
import Detail from "./detail/index.vue";
|
||||||
|
import myEvent from '@/utils/myEvent'
|
||||||
//const props = defineProps({
|
//const props = defineProps({
|
||||||
//})
|
//})
|
||||||
//const emit = defineEmits([
|
//const emit = defineEmits([
|
||||||
//])
|
//])
|
||||||
let data = reactive({
|
let data = reactive({
|
||||||
})
|
})
|
||||||
|
const addShopping = (item) => {
|
||||||
|
myEvent.emit('addShopping', item)
|
||||||
|
}
|
||||||
onMounted(()=>{
|
onMounted(()=>{
|
||||||
})
|
})
|
||||||
onUnmounted(()=>{
|
onUnmounted(()=>{
|
||||||
@@ -25,29 +28,20 @@ const {} = toRefs(data);
|
|||||||
</div>
|
</div>
|
||||||
<div class="title-content">
|
<div class="title-content">
|
||||||
<div class="title-box">
|
<div class="title-box">
|
||||||
<div class="left">
|
<div class="title">
|
||||||
<div class="title">
|
We’re Seeking
|
||||||
Windswept Burden
|
|
||||||
</div>
|
|
||||||
<div class="info">
|
|
||||||
Publish Date: 24th Nov 2025
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="right">
|
<div class="info">
|
||||||
<div class="info">
|
Fashion Voice Worth Featuring.
|
||||||
“We are spiritual nomads carrying what wind cannot take. <br />
|
|
||||||
Inspired by those who knew home is not a place, but what you wear.”
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="scrolling-learn-more">
|
<div class="button">
|
||||||
<div>Scrolling Learn More</div>
|
<a href="mailto:info@code-create.com.hk">Contact Us if Interested</a>
|
||||||
<SvgIcon name="collectionStory-scrollingLearnMore" size="48" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<Detail></Detail>
|
<Detail @addShopping="addShopping"></Detail>
|
||||||
</div>
|
</div>
|
||||||
<Footer></Footer>
|
<Footer></Footer>
|
||||||
</div>
|
</div>
|
||||||
@@ -70,7 +64,7 @@ const {} = toRefs(data);
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: .8rem;
|
gap: .8rem;
|
||||||
color: #fff;
|
color: #000;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
> .text{
|
> .text{
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
@@ -80,73 +74,46 @@ const {} = toRefs(data);
|
|||||||
}
|
}
|
||||||
> .title-content{
|
> .title-content{
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 63.2rem;
|
|
||||||
margin-top: 24.8rem;
|
|
||||||
background: linear-gradient(180deg, rgba(0, 0, 0, 0) 38.37%, rgba(0, 0, 0, 0.192) 90.74%);
|
|
||||||
padding: 0 4rem;
|
|
||||||
> .title-box{
|
|
||||||
margin-top: 36.7rem;
|
|
||||||
color: #fff;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
> .left{
|
|
||||||
font-family: 'KaiseiOpti-Bold';
|
|
||||||
font-weight: 700;
|
|
||||||
> .title{
|
|
||||||
font-size: 6rem;
|
|
||||||
line-height: 6rem;
|
|
||||||
}
|
|
||||||
> .info{
|
|
||||||
margin-top: 1.7rem;
|
|
||||||
font-size: 1.8rem;
|
|
||||||
line-height: 100%;
|
|
||||||
vertical-align: bottom;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
> .right{
|
|
||||||
> .info{
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 1.8rem;
|
|
||||||
line-height: 100%;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.scrolling-learn-more{
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 2.1rem;
|
padding: 0 6.7rem;
|
||||||
left: 50%;
|
margin-top: 11.5rem;
|
||||||
transform: translateX(-50%);
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
color: #fff;
|
align-items: flex-start;
|
||||||
animation: scroll 3s linear infinite;
|
> .title-box{
|
||||||
@keyframes scroll {
|
display: flex;
|
||||||
0% {
|
flex-direction: column;
|
||||||
transform: translateY(0);
|
> .title{
|
||||||
|
font-size: 6.5rem;
|
||||||
|
line-height: 100%;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #585858;
|
||||||
}
|
}
|
||||||
50% {
|
> .info{
|
||||||
transform: translateY(-20%);
|
font-size: 3rem;
|
||||||
}
|
font-weight: 500;
|
||||||
100% {
|
line-height: 100%;
|
||||||
transform: translateY(0%);
|
color: #585858;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
> div{
|
> .button{
|
||||||
font-family: 'KaiseiOpti-Regular';
|
padding: 0 4.5rem;
|
||||||
font-weight: 400;
|
line-height: 5.1rem;
|
||||||
font-size: 1.4rem;
|
background-color: #1B1B1B;
|
||||||
line-height: 100%;
|
color: #fff;
|
||||||
text-align: center;
|
margin-top: 4rem;
|
||||||
margin-bottom: 1.5rem;
|
font-weight: 700;
|
||||||
white-space: nowrap;
|
font-size: 2rem;
|
||||||
|
letter-spacing: -0.4px;
|
||||||
|
cursor: pointer;
|
||||||
|
> a{
|
||||||
|
color: #fff;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.banner{
|
.banner{
|
||||||
width: 100%;
|
width: 100%;
|
||||||
position: absolute;
|
|
||||||
z-index: -1;
|
z-index: -1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
310
src/views/digitalDetail/index.vue
Normal file
@@ -0,0 +1,310 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, onUnmounted, reactive, toRefs } from "vue";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
import img from "@/assets/images/collectionStory/Rectangle.png";
|
||||||
|
import myEvent from '@/utils/myEvent'
|
||||||
|
|
||||||
|
|
||||||
|
//const props = defineProps({
|
||||||
|
//})
|
||||||
|
//const emit = defineEmits([
|
||||||
|
//])
|
||||||
|
const router = useRouter()
|
||||||
|
let data = reactive({
|
||||||
|
})
|
||||||
|
const addShopping = (item) => {
|
||||||
|
myEvent.emit('addShopping', item)
|
||||||
|
}
|
||||||
|
onMounted(()=>{
|
||||||
|
})
|
||||||
|
onUnmounted(()=>{
|
||||||
|
})
|
||||||
|
defineExpose({})
|
||||||
|
const {} = toRefs(data);
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="digitalItemDetail">
|
||||||
|
<div class="center">
|
||||||
|
<div class="img-list">
|
||||||
|
<div class="title">
|
||||||
|
<div>Sketch</div>
|
||||||
|
<div>Illustration</div>
|
||||||
|
<div>Product</div>
|
||||||
|
</div>
|
||||||
|
<div class="img">
|
||||||
|
<div class="sketch">
|
||||||
|
<img :src="img" v-for="item in 4" :key="item" alt="">
|
||||||
|
</div>
|
||||||
|
<div class="illustration">
|
||||||
|
<img :src="img" v-for="item in 4" :key="item" alt="">
|
||||||
|
</div>
|
||||||
|
<div class="product">
|
||||||
|
<img :src="img" v-for="item in 4" :key="item" alt="">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="img-detail">
|
||||||
|
<div class="back" @click="router.back()">
|
||||||
|
<div class="icon">
|
||||||
|
<svg-icon name="digital-back" size="28"></svg-icon>
|
||||||
|
</div>
|
||||||
|
<span>Back</span>
|
||||||
|
</div>
|
||||||
|
<div class="img-info">
|
||||||
|
<div class="img-type">FEMALE / skirt, blouse, Outwear</div>
|
||||||
|
<div class="img-name">Heritage Layered Set</div>
|
||||||
|
<div class="img-price">$100 <span class="mini-scrollbar">HKD</span></div>
|
||||||
|
</div>
|
||||||
|
<div class="commodity">
|
||||||
|
<div class="info">
|
||||||
|
<img class="profile" :src="img" alt="">
|
||||||
|
<div class="detail">
|
||||||
|
<div class="name">Roaming Clouds</div>
|
||||||
|
<div class="release-time">
|
||||||
|
<span>Release in Feb 26, 2026</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="introduce">
|
||||||
|
This ensemble artfully merges traditional folk heritage with contemporary tailoring, creating a timeless silhouette that honors ancestral craftsmanship while embracing modern sophistication.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="notice">
|
||||||
|
<div class="title">Copyright & License Notice</div>
|
||||||
|
<div class="conter">
|
||||||
|
<div class="contet-title">
|
||||||
|
<div class="icon">
|
||||||
|
<svg-icon name="digital-Info" size="24"></svg-icon>
|
||||||
|
</div>
|
||||||
|
<span>License Included in Asset</span>
|
||||||
|
</div>
|
||||||
|
<div class="info">
|
||||||
|
All products on this platform are digital assets, not physical goods. Purchase grants a usage license only; copyright and intellectual property rights remain with the original creator, unless otherwise stated.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="button">
|
||||||
|
<div class="buy-now">Buy Now</div>
|
||||||
|
<div class="add-cart" @click="addShopping(item)">
|
||||||
|
<div class="icon">
|
||||||
|
<svg-icon name="cart_0" size="24"></svg-icon>
|
||||||
|
</div>
|
||||||
|
Add to Cart
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Footer></Footer>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
:deep(.el-drawer__body){
|
||||||
|
--el-drawer-padding-primary: 2.4rem 3.4rem 2.4rem 6rem;
|
||||||
|
}
|
||||||
|
.digitalItemDetail{
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
.center{
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
.img-list{
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
--row-width: 33.333%;
|
||||||
|
// --row-width: 29.3rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
&::-webkit-scrollbar{
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
> .title{
|
||||||
|
display: flex;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
> div{
|
||||||
|
width: var(--row-width);
|
||||||
|
line-height: 8.6rem;
|
||||||
|
padding-left: 2.4rem;
|
||||||
|
border-right: 0.5px solid #C4C4C4;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 1.6rem;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
&:last-child{
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
> .img{
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
> div{
|
||||||
|
display: flex;
|
||||||
|
width: var(--row-width);
|
||||||
|
border-right: 0.5px solid #C4C4C4;
|
||||||
|
flex-direction: column;
|
||||||
|
&:last-child{
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.img-detail{
|
||||||
|
border-left: 0.5px solid #585858;
|
||||||
|
padding-left: 3.2rem;
|
||||||
|
width: 57rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow-y: auto;
|
||||||
|
&::-webkit-scrollbar{
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.back{
|
||||||
|
display: flex;
|
||||||
|
margin-top: 2.8rem;
|
||||||
|
gap: 1.4rem;
|
||||||
|
align-items: center;
|
||||||
|
width: min-content;
|
||||||
|
cursor: pointer;
|
||||||
|
> span{
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 1.6rem;
|
||||||
|
line-height: 120%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.img-info{
|
||||||
|
margin-top: 2.8rem;
|
||||||
|
.img-type{
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
line-height: 120%;
|
||||||
|
color: #808080;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
.img-name{
|
||||||
|
font-family: KaiseiOpti-Bold;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 3rem;
|
||||||
|
line-height: 120%;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
.img-price{
|
||||||
|
font-family: KaiseiOpti-Bold;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 3rem;
|
||||||
|
line-height: 120%;
|
||||||
|
> span{
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 2rem;
|
||||||
|
line-height: 120%;
|
||||||
|
color: #585858;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.commodity{
|
||||||
|
margin-top: 4rem;
|
||||||
|
.info{
|
||||||
|
display: flex;
|
||||||
|
gap: 1.4rem;
|
||||||
|
margin-bottom: 1.4rem;
|
||||||
|
> .profile{
|
||||||
|
width: 5.4rem;
|
||||||
|
height: 5.4rem;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
> .detail{
|
||||||
|
.name{
|
||||||
|
text-decoration: underline;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 1.8rem;
|
||||||
|
line-height: 100%;
|
||||||
|
margin-bottom: .8rem;
|
||||||
|
}
|
||||||
|
.release-time{
|
||||||
|
font-family: KaiseiOpti-Regular;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
line-height: 140%;
|
||||||
|
color: #585858;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.introduce{
|
||||||
|
font-family: KaiseiOpti-Regular;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 140%;
|
||||||
|
color: #585858;
|
||||||
|
width: 50.8rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.notice{
|
||||||
|
margin-top: 6rem;
|
||||||
|
.title{
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 1.8rem;
|
||||||
|
line-height: 140%;
|
||||||
|
}
|
||||||
|
.conter{
|
||||||
|
width: 50.8rem;
|
||||||
|
background-color: #f6f6f6;
|
||||||
|
padding: 2rem;
|
||||||
|
.contet-title{
|
||||||
|
margin-bottom: 1.2rem;
|
||||||
|
display: flex;
|
||||||
|
gap: .8rem;
|
||||||
|
align-items: center;
|
||||||
|
> span{
|
||||||
|
font-family: KaiseiOpti-Regular;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
line-height: 140%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.info{
|
||||||
|
font-family: KaiseiOpti-Regular;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
line-height: 140%;
|
||||||
|
color: #585858;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.button{
|
||||||
|
width: 50.8rem;
|
||||||
|
margin-top: auto;
|
||||||
|
> div{
|
||||||
|
width: 100%;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 1.6rem;
|
||||||
|
line-height: 120%;
|
||||||
|
letter-spacing: 3%;
|
||||||
|
border: 1px solid #232323;
|
||||||
|
line-height: 4.8rem;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
.buy-now{
|
||||||
|
background-color: #232323;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.add-cart{
|
||||||
|
background-color: #fff;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
101
src/views/digitalItem/commodity-list.vue
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, onUnmounted, reactive, toRefs } from "vue";
|
||||||
|
import img from "@/assets/images/collectionStory/Rectangle.png";
|
||||||
|
//const props = defineProps({
|
||||||
|
//})
|
||||||
|
const emit = defineEmits([
|
||||||
|
'addShopping',
|
||||||
|
'openDetail'
|
||||||
|
])
|
||||||
|
let data = reactive({
|
||||||
|
})
|
||||||
|
const list = ref([
|
||||||
|
{
|
||||||
|
url: img,
|
||||||
|
title: "Windswept Burden",
|
||||||
|
price: "$100.00",
|
||||||
|
},{
|
||||||
|
url: img,
|
||||||
|
title: "Windswept Burden",
|
||||||
|
price: "$100.00",
|
||||||
|
},{
|
||||||
|
url: img,
|
||||||
|
title: "Windswept Burden",
|
||||||
|
price: "$100.00",
|
||||||
|
},{
|
||||||
|
url: img,
|
||||||
|
title: "Windswept Burden",
|
||||||
|
price: "$100.00",
|
||||||
|
},{
|
||||||
|
url: img,
|
||||||
|
title: "Windswept Burden",
|
||||||
|
price: "$100.00",
|
||||||
|
},{
|
||||||
|
url: img,
|
||||||
|
title: "Windswept Burden",
|
||||||
|
price: "$100.00",
|
||||||
|
},{
|
||||||
|
url: img,
|
||||||
|
title: "Windswept Burden",
|
||||||
|
price: "$100.00",
|
||||||
|
},{
|
||||||
|
url: img,
|
||||||
|
title: "Windswept Burden",
|
||||||
|
price: "$100.00",
|
||||||
|
},
|
||||||
|
])
|
||||||
|
const type = ref('All')
|
||||||
|
const addShopping = (item) => {
|
||||||
|
emit('addShopping', item)
|
||||||
|
}
|
||||||
|
const openDetail = (item) => {
|
||||||
|
emit('openDetail', item)
|
||||||
|
}
|
||||||
|
onMounted(()=>{
|
||||||
|
})
|
||||||
|
onUnmounted(()=>{
|
||||||
|
})
|
||||||
|
defineExpose({})
|
||||||
|
const {} = toRefs(data);
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="commodityList">
|
||||||
|
<div class="list">
|
||||||
|
<div class="item" v-for="item in list" :key="item.url">
|
||||||
|
<CommodityItem :url="item.url" :name="item.title" :price="item.price" @addShopping="addShopping(item)" @openDetail="openDetail(item)"></CommodityItem>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.commodityList{
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
.list{
|
||||||
|
width: 100%;
|
||||||
|
flex: 1;
|
||||||
|
// display: grid;
|
||||||
|
// align-content: start;
|
||||||
|
// grid-template-columns: repeat(3, 1fr);
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
display: grid;
|
||||||
|
align-content: start;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(min(100%, 28rem), 1fr));
|
||||||
|
border-top: 0.5px solid #585858;
|
||||||
|
padding: .5px 0 0 .5px;
|
||||||
|
/* 垂直线(右边框) */
|
||||||
|
.item{
|
||||||
|
position: relative;
|
||||||
|
padding: 1.2rem;
|
||||||
|
border-bottom: 0.5px solid #585858;
|
||||||
|
border-right: 0.5px solid #585858;
|
||||||
|
margin-right: -1px;
|
||||||
|
margin-bottom: -1px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
217
src/views/digitalItem/index.vue
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, onUnmounted, reactive, toRefs, onActivated } from "vue";
|
||||||
|
import CommodityList from "./commodity-list.vue";
|
||||||
|
import MerchantInfo from "./merchant-info.vue";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
import scListNull from '@/views/shoppingCart/sc-list-null.vue'
|
||||||
|
|
||||||
|
// 定义组件名称
|
||||||
|
defineOptions({
|
||||||
|
name: 'digitalItem'
|
||||||
|
})
|
||||||
|
//const props = defineProps({
|
||||||
|
//})
|
||||||
|
//const emit = defineEmits([
|
||||||
|
//])
|
||||||
|
const digitalItemRef = ref(null)
|
||||||
|
const scrollTop = ref(0)
|
||||||
|
const router = useRouter()
|
||||||
|
let data = reactive({
|
||||||
|
})
|
||||||
|
|
||||||
|
const categoriesList = ref([
|
||||||
|
{
|
||||||
|
value:'Best Selling',
|
||||||
|
label:'Best Selling'
|
||||||
|
},{
|
||||||
|
value:'Price: Low to High',
|
||||||
|
label:'Price: Low to High'
|
||||||
|
},{
|
||||||
|
value:'Newest First',
|
||||||
|
label:'Newest First'
|
||||||
|
},
|
||||||
|
])
|
||||||
|
const categories = ref('Newest First')
|
||||||
|
const addShopping = (item) => {}
|
||||||
|
const openDetail = (item) => {
|
||||||
|
scrollTop.value = digitalItemRef.value.scrollTop
|
||||||
|
router.push({
|
||||||
|
path: '/digitalItem/' + 123,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
onActivated(()=>{
|
||||||
|
digitalItemRef.value.scrollTop = scrollTop.value
|
||||||
|
})
|
||||||
|
onMounted(()=>{
|
||||||
|
})
|
||||||
|
onUnmounted(()=>{
|
||||||
|
})
|
||||||
|
defineExpose({})
|
||||||
|
const {} = toRefs(data);
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="digitalItem" ref="digitalItemRef">
|
||||||
|
<div class="header-img">
|
||||||
|
<img src="@/assets/images/digitalItem/digital_item_banner.png" alt="">
|
||||||
|
<div class="text">
|
||||||
|
<div class="title">Digital Item</div>
|
||||||
|
<p class="info">Virtual fashion creations collected in your personal archive</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="filters">
|
||||||
|
<div class="filter-item">
|
||||||
|
<el-select v-model="categories" placeholder="Sort By" :teleported="false">
|
||||||
|
<template #label="{ label }">
|
||||||
|
<span class="header-label">Sort By</span>
|
||||||
|
<span class="header-value">{{ label }}</span>
|
||||||
|
</template>
|
||||||
|
<el-option
|
||||||
|
v-for="item in categoriesList"
|
||||||
|
:key="item.label"
|
||||||
|
:value="item.value"
|
||||||
|
:label="item.label"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<div class="merchant-info">
|
||||||
|
<MerchantInfo></MerchantInfo>
|
||||||
|
</div>
|
||||||
|
<div class="commodity-list">
|
||||||
|
<CommodityList v-if="true" @addShopping="addShopping" @openDetail="openDetail"></CommodityList>
|
||||||
|
<div v-else class="null">
|
||||||
|
<sc-list-null
|
||||||
|
nullImage="shopping-cart"
|
||||||
|
:showButton="false"
|
||||||
|
title="Nothing in Digital Item"
|
||||||
|
tip="Try adjusting your filters or refreshing the page."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Footer></Footer>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.digitalItem{
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
overflow-y: auto;
|
||||||
|
.header-img{
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
>img{
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
> .text{
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
> .title{
|
||||||
|
font-family: KaiseiOpti-Bold;
|
||||||
|
color: #232323;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 4rem;
|
||||||
|
line-height: 2.3rem;
|
||||||
|
letter-spacing: 0%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
> .info{
|
||||||
|
font-family: KaiseiOpti-Regular;
|
||||||
|
color: #585858;
|
||||||
|
font-size: 1.6rem;
|
||||||
|
line-height: 140%;
|
||||||
|
margin-top: 1.2rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
> .filters{
|
||||||
|
width: 100%;
|
||||||
|
height: 6rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 9rem;
|
||||||
|
justify-content: flex-end;
|
||||||
|
> .filter-item{
|
||||||
|
:deep(.el-select) {
|
||||||
|
width: 15rem;
|
||||||
|
--el-border-radius-base: 0;
|
||||||
|
--el-select-input-color: rgba(0, 0, 0, 0.5);
|
||||||
|
--el-select-input-font-size: 1rem;
|
||||||
|
.el-select__wrapper {
|
||||||
|
font-size: 1.07rem;
|
||||||
|
padding: 0 0.7rem;
|
||||||
|
line-height: 1;
|
||||||
|
min-height: 0;
|
||||||
|
height: 2.2rem;
|
||||||
|
|
||||||
|
.header-label {
|
||||||
|
font-family: KaiseiOpti-Regular;
|
||||||
|
color: rgba(0, 0, 0, 0.5);
|
||||||
|
margin-right: 0.6rem;
|
||||||
|
}
|
||||||
|
.header-value {
|
||||||
|
font-family: KaiseiOpti-Bold;
|
||||||
|
color: #232323;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.el-select__popper {
|
||||||
|
--el-popper-border-radius: 0;
|
||||||
|
border: 0.1rem solid #d0d0d0;
|
||||||
|
.el-select-dropdown__list {
|
||||||
|
padding: 0;
|
||||||
|
> .el-select-dropdown__item {
|
||||||
|
margin-bottom: 0.89rem;
|
||||||
|
color: #232323;
|
||||||
|
font-size: 1.069rem;
|
||||||
|
height: 2.68rem;
|
||||||
|
line-height: 2.68rem;
|
||||||
|
padding: 0 1.4rem;
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
&.is-selected {
|
||||||
|
font-family: KaiseiOpti-Bold;
|
||||||
|
background-color: #f4f4f4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
> .content{
|
||||||
|
display: flex;
|
||||||
|
height: auto;
|
||||||
|
// align-items: flex-start;
|
||||||
|
border-top: 0.5px solid #585858;
|
||||||
|
.merchant-info{
|
||||||
|
width: 38.5rem;
|
||||||
|
padding-left: 10.2rem;
|
||||||
|
height: var(--app-view-height);
|
||||||
|
overflow-y: auto;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
&::-webkit-scrollbar{
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.commodity-list{
|
||||||
|
flex: 1;
|
||||||
|
border-left: 0.5px solid #585858;
|
||||||
|
border-right: 0.5px solid #585858;
|
||||||
|
margin-right: 9rem;
|
||||||
|
display: flex;
|
||||||
|
.null{
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
127
src/views/digitalItem/merchant-info.vue
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, onUnmounted, reactive, toRefs } from "vue";
|
||||||
|
//const props = defineProps({
|
||||||
|
//})
|
||||||
|
//const emit = defineEmits([
|
||||||
|
//])
|
||||||
|
let data = reactive({
|
||||||
|
})
|
||||||
|
const categoriesList = ref([
|
||||||
|
{
|
||||||
|
label: 'Outwear',
|
||||||
|
value: 'Outwear'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Dress',
|
||||||
|
value: 'Dress'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Trousers',
|
||||||
|
value: 'Trousers'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Blouse',
|
||||||
|
value: 'Blouse'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Skirt',
|
||||||
|
value: 'Skirt'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Accessories',
|
||||||
|
value: 'Accessories'
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const genderList = ref([
|
||||||
|
{
|
||||||
|
label: 'Male',
|
||||||
|
value: 'Male'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Female',
|
||||||
|
value: 'Female'
|
||||||
|
},
|
||||||
|
])
|
||||||
|
const categories = ref([''])
|
||||||
|
const gender = ref([''])
|
||||||
|
|
||||||
|
const clearFilters = () => {
|
||||||
|
categories.value = ['']
|
||||||
|
gender.value = ['']
|
||||||
|
}
|
||||||
|
onMounted(()=>{
|
||||||
|
})
|
||||||
|
onUnmounted(()=>{
|
||||||
|
})
|
||||||
|
defineExpose({})
|
||||||
|
const {} = toRefs(data);
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="filters">
|
||||||
|
<div class="title">
|
||||||
|
<div class="left">Filters</div>
|
||||||
|
<div class="right" @click="clearFilters">Clear</div>
|
||||||
|
</div>
|
||||||
|
<div class="categories">Categories</div>
|
||||||
|
<div class="line"></div>
|
||||||
|
<div class="multiple">
|
||||||
|
<checked :list="categoriesList" v-model:selected="categories" />
|
||||||
|
</div>
|
||||||
|
<div class="categories">Gender</div>
|
||||||
|
<div class="line"></div>
|
||||||
|
<div class="multiple">
|
||||||
|
<checked :list="genderList" v-model:selected="gender" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.filters{
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
padding-top: 4rem;
|
||||||
|
padding-bottom: 4rem;
|
||||||
|
.title{
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
display: flex;
|
||||||
|
padding: 0 1.2rem;
|
||||||
|
.left{
|
||||||
|
margin-right: 12.2rem;
|
||||||
|
font-family: "KaiseiOpti-Bold";
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 2.4rem;
|
||||||
|
line-height: 3.5rem;
|
||||||
|
color: #232323;
|
||||||
|
}
|
||||||
|
.right{
|
||||||
|
text-decoration: underline;
|
||||||
|
font-family: "KaiseiOpti-Regular";
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 1.6rem;
|
||||||
|
line-height: 2.4rem;
|
||||||
|
letter-spacing: -0.48px;
|
||||||
|
text-align: right;
|
||||||
|
color: #979797;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.categories{
|
||||||
|
font-family: "KaiseiOpti-Bold";
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 1.8rem;
|
||||||
|
line-height: 2.3rem;
|
||||||
|
color: #585858;
|
||||||
|
margin-bottom: 1.1rem;
|
||||||
|
padding: 0 1.2rem;
|
||||||
|
}
|
||||||
|
.line{
|
||||||
|
border-top: 0.5px solid #C4C4C4;
|
||||||
|
width: 27.1rem;
|
||||||
|
margin-bottom: 2.2rem;
|
||||||
|
}
|
||||||
|
.multiple{
|
||||||
|
padding: 0 2.3rem;
|
||||||
|
margin-bottom: 2.9rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="home-index">
|
<div class="home-index">
|
||||||
<section-index />
|
<section-index />
|
||||||
<section-designers />
|
<section-designer />
|
||||||
<section-design />
|
<section-design />
|
||||||
<section-digital-items1 />
|
<section-digital-items1 />
|
||||||
<section-digital-items2 />
|
<section-digital-items2 />
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import SectionIndex from './section-index.vue'
|
import SectionIndex from './section-index.vue'
|
||||||
import SectionDesigners from './section-designers.vue'
|
import SectionDesigner from './section-designer.vue'
|
||||||
import SectionDesign from './section-design.vue'
|
import SectionDesign from './section-design.vue'
|
||||||
import SectionDigitalItems1 from './section-digital-items1.vue'
|
import SectionDigitalItems1 from './section-digital-items1.vue'
|
||||||
import SectionDigitalItems2 from './section-digital-items2.vue'
|
import SectionDigitalItems2 from './section-digital-items2.vue'
|
||||||
|
|||||||
51
src/views/home/section-designer.vue
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
<template>
|
||||||
|
<section class="section-designer">
|
||||||
|
<div class="title">Designer Community</div>
|
||||||
|
<div class="tip">
|
||||||
|
Discover the designers shaping AiDA’s creative landscape. <br />
|
||||||
|
Each month, we will showcase a curated selection of their most distinguished works.
|
||||||
|
</div>
|
||||||
|
<button custom="black" @click="onSearchBrand">Search Brands</button>
|
||||||
|
<img src="@/assets/images/home/designer-bg.png" />
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
const router = useRouter()
|
||||||
|
const onSearchBrand = () => {
|
||||||
|
router.push({ name: 'brand' })
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less">
|
||||||
|
.section-designer {
|
||||||
|
padding: 9rem 8rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
> .title {
|
||||||
|
font-family: KaiseiOpti-Bold;
|
||||||
|
font-size: 5.6rem;
|
||||||
|
line-height: 6.2rem;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
> .tip {
|
||||||
|
font-family: KaiseiOpti-Regular;
|
||||||
|
font-size: 2rem;
|
||||||
|
line-height: 2.8rem;
|
||||||
|
text-align: center;
|
||||||
|
color: #979797;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
> button {
|
||||||
|
margin-bottom: 4.6rem;
|
||||||
|
}
|
||||||
|
> img {
|
||||||
|
width: 73%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
<template>
|
|
||||||
<section class="section-designers">
|
|
||||||
<div class="title">Popular Designers</div>
|
|
||||||
<div class="tip">
|
|
||||||
Discover the designers shaping AiDA’s creative landscape,<br />as we present their most
|
|
||||||
distinguished works each month.
|
|
||||||
</div>
|
|
||||||
<div class="content">
|
|
||||||
<img src="@/assets/images/home/designers-left.jpg" />
|
|
||||||
<div class="box">
|
|
||||||
<div class="intro">
|
|
||||||
<span>{{ list[index]?.intro || '' }}</span>
|
|
||||||
<img src="@/assets/images/home/designers-right.jpg" />
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="name-item"
|
|
||||||
v-for="(v, i) in list"
|
|
||||||
:key="i"
|
|
||||||
:class="{ active: i === index }"
|
|
||||||
@click="index = i"
|
|
||||||
>
|
|
||||||
<span class="name">{{ v.name }}</span>
|
|
||||||
<span class="icon">
|
|
||||||
<svg-icon name="arrow_right" size="20" />
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { computed, ref } from 'vue'
|
|
||||||
const index = ref(0)
|
|
||||||
const list = ref([
|
|
||||||
{
|
|
||||||
name: 'Ji-Yeon Park',
|
|
||||||
intro:
|
|
||||||
'Through its fragile tulle layers over a grounded silhouette, the garment reflects the tension between vulnerability and strength within one’s journey.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Lian Su',
|
|
||||||
intro: '阿巴阿巴~'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Céline Moreau',
|
|
||||||
intro: '这是Céline Moreau的设计~'
|
|
||||||
}
|
|
||||||
])
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="less">
|
|
||||||
.section-designers {
|
|
||||||
padding: 9rem 8rem;
|
|
||||||
> .title {
|
|
||||||
font-family: KaiseiOpti-Bold;
|
|
||||||
font-size: 5.6rem;
|
|
||||||
line-height: 6.2rem;
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
> .tip {
|
|
||||||
font-family: KaiseiOpti-Regular;
|
|
||||||
font-size: 2rem;
|
|
||||||
line-height: 2.8rem;
|
|
||||||
text-align: center;
|
|
||||||
color: #979797;
|
|
||||||
}
|
|
||||||
> .content {
|
|
||||||
margin-top: 5rem;
|
|
||||||
border: 0.1rem solid #979797;
|
|
||||||
border-left: none;
|
|
||||||
border-left: none;
|
|
||||||
border-right: none;
|
|
||||||
box-sizing: content-box;
|
|
||||||
--height: 45rem;
|
|
||||||
min-height: var(--height);
|
|
||||||
display: flex;
|
|
||||||
> img {
|
|
||||||
width: var(--height);
|
|
||||||
height: var(--height);
|
|
||||||
margin: 2.4rem 2.4rem 2.4rem 0;
|
|
||||||
}
|
|
||||||
> .box {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
> .intro {
|
|
||||||
display: flex;
|
|
||||||
margin: 2rem 2rem 2rem 0;
|
|
||||||
> span {
|
|
||||||
flex: 1;
|
|
||||||
font-family: KaiseiOpti-Regular;
|
|
||||||
font-size: 2rem;
|
|
||||||
line-height: 3rem;
|
|
||||||
color: #232323;
|
|
||||||
margin-right: 9rem;
|
|
||||||
}
|
|
||||||
> img {
|
|
||||||
width: 25rem;
|
|
||||||
height: 25rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
> .name-item {
|
|
||||||
flex: 1;
|
|
||||||
min-height: 5rem;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
border-top: 0.1rem solid #7b7b7b;
|
|
||||||
padding: 0 2.1rem;
|
|
||||||
font-family: KaiseiOpti-Regular;
|
|
||||||
font-size: 2.4rem;
|
|
||||||
color: #7b7b7b;
|
|
||||||
> .icon {
|
|
||||||
transform: rotate(-45deg);
|
|
||||||
}
|
|
||||||
&.active {
|
|
||||||
font-family: KaiseiOpti-Bold;
|
|
||||||
color: #232323;
|
|
||||||
background: #f6f6f6;
|
|
||||||
> .icon {
|
|
||||||
transform: rotate(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
<div class="mate">
|
<div class="mate">
|
||||||
<div class="logos">
|
<div class="logos">
|
||||||
<img src="@/assets/images/logos/code-create-black.png" />
|
<img src="@/assets/images/logos/code-create-black.png" />
|
||||||
<img src="@/assets/images/logos/stylish-arade-black.png" />
|
<img src="@/assets/images/logos/stylish-parade-black.png" />
|
||||||
<img src="@/assets/images/logos/aida-black.png" />
|
<img src="@/assets/images/logos/aida-black.png" />
|
||||||
</div>
|
</div>
|
||||||
<div class="tip">
|
<div class="tip">
|
||||||
|
|||||||
@@ -1,74 +1,58 @@
|
|||||||
<template>
|
<template>
|
||||||
<section class="section-index bgw">
|
<section class="section-index bgw">
|
||||||
<img src="@/assets/images/home/bg.jpg" class="bg" />
|
<img src="@/assets/images/home/bg.png" class="bg" />
|
||||||
<div class="shade-1"></div>
|
|
||||||
<div class="shade-2"></div>
|
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<div class="title">Windswept Burden</div>
|
<div class="title" v-html="title"></div>
|
||||||
<div class="tip">We are spiritual nomads carrying<br />what wind cannot take.</div>
|
<div class="tip">
|
||||||
<button custom>View More</button>
|
Discover collections through the stories behind their creation. A curated space connecting
|
||||||
<div class="aida-logo"><img src="@/assets/images/logos/aida.png" /></div>
|
designers, narratives, and fashion commerce.
|
||||||
<p class="tip">
|
</div>
|
||||||
What you wear is how you present yourself to the world, especially today, when human
|
<button custom="black-box" @click="handleClickArrow">
|
||||||
contacts are so quick. Fashion is instant language
|
<svg-icon name="arrow_right" size="34" />
|
||||||
</p>
|
</button>
|
||||||
<p class="tip">I firmly believe that with the right footwear one can rule the world.</p>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
const router = useRouter()
|
||||||
|
const title =
|
||||||
|
'We’re Seeking<br /><span>Fashion Voice</span><br /><span class="small">Worth Featuring.</span>'
|
||||||
|
const handleClickArrow = () => {
|
||||||
|
router.push({ name: 'collectionStory' })
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less">
|
<style lang="less">
|
||||||
.section-index {
|
.section-index {
|
||||||
> .shade-1 {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 96rem;
|
|
||||||
height: 100%;
|
|
||||||
background: linear-gradient(to left, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.25) 100%);
|
|
||||||
}
|
|
||||||
> .shade-2 {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 30rem;
|
|
||||||
background: linear-gradient(180deg, rgba(0, 0, 0, 0) 38.37%, rgba(0, 0, 0, 0.192) 90.74%);
|
|
||||||
}
|
|
||||||
> .content {
|
> .content {
|
||||||
top: 16rem;
|
top: 13.7rem;
|
||||||
left: 9rem;
|
left: 9rem;
|
||||||
color: #fff;
|
color: #232323;
|
||||||
> .title {
|
> .title {
|
||||||
font-family: KaiseiOpti-Bold;
|
font-family: KaiseiOpti-Bold;
|
||||||
font-size: 6rem;
|
font-size: 7rem;
|
||||||
margin-bottom: 3rem;
|
margin-bottom: 3rem;
|
||||||
}
|
span {
|
||||||
> div.tip {
|
font-family: KaiseiOpti-Medium;
|
||||||
font-family: KaiseiOpti-Regular;
|
&.small {
|
||||||
font-size: 3.2rem;
|
font-size: 6rem;
|
||||||
line-height: 4.3rem;
|
}
|
||||||
}
|
|
||||||
> button {
|
|
||||||
margin-top: 11.6rem;
|
|
||||||
margin-bottom: 13.9rem;
|
|
||||||
}
|
|
||||||
> .aida-logo {
|
|
||||||
margin-bottom: 3rem;
|
|
||||||
> img {
|
|
||||||
width: auto;
|
|
||||||
height: 5rem;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
> p.tip {
|
> .tip {
|
||||||
font-family: KaiseiOpti-Regular;
|
width: 50rem;
|
||||||
font-size: 1.2rem;
|
font-size: 1.8rem;
|
||||||
line-height: 2rem;
|
line-height: 2.6rem;
|
||||||
color: #ededed;
|
color: #585858;
|
||||||
|
}
|
||||||
|
> button {
|
||||||
|
margin-top: 12rem;
|
||||||
|
min-width: 0;
|
||||||
|
width: 8rem;
|
||||||
|
height: 8rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@
|
|||||||
.register:deep(.el-form) .el-input,
|
.register:deep(.el-form) .el-input,
|
||||||
.login:deep(.el-form) .el-input {
|
.login:deep(.el-form) .el-input {
|
||||||
--el-input-height: 3.4rem;
|
--el-input-height: 3.4rem;
|
||||||
|
--el-input-inner-height: auto;
|
||||||
--el-input-border-radius: 0;
|
--el-input-border-radius: 0;
|
||||||
--el-input-text-color: #232323;
|
--el-input-text-color: #232323;
|
||||||
--el-border-color: #C4C4C4;
|
--el-border-color: #C4C4C4;
|
||||||
@@ -45,6 +46,11 @@
|
|||||||
.login:deep(.el-form) .el-input::placeholder {
|
.login:deep(.el-form) .el-input::placeholder {
|
||||||
color: #9F9F9F;
|
color: #9F9F9F;
|
||||||
}
|
}
|
||||||
|
.retrieve-password:deep(.el-form) .el-input .el-input__wrapper,
|
||||||
|
.register:deep(.el-form) .el-input .el-input__wrapper,
|
||||||
|
.login:deep(.el-form) .el-input .el-input__wrapper {
|
||||||
|
padding: 0.1rem 1rem;
|
||||||
|
}
|
||||||
.retrieve-password:deep(.el-form) .password-tip,
|
.retrieve-password:deep(.el-form) .password-tip,
|
||||||
.register:deep(.el-form) .password-tip,
|
.register:deep(.el-form) .password-tip,
|
||||||
.login:deep(.el-form) .password-tip {
|
.login:deep(.el-form) .password-tip {
|
||||||
@@ -108,16 +114,47 @@
|
|||||||
margin-top: -0.6rem;
|
margin-top: -0.6rem;
|
||||||
--el-checkbox-height: auto;
|
--el-checkbox-height: auto;
|
||||||
}
|
}
|
||||||
.retrieve-password:deep(.el-form) .privacy .el-checkbox__label,
|
.retrieve-password:deep(.el-form) .privacy .el-checkbox,
|
||||||
.register:deep(.el-form) .privacy .el-checkbox__label,
|
.register:deep(.el-form) .privacy .el-checkbox,
|
||||||
.login:deep(.el-form) .privacy .el-checkbox__label {
|
.login:deep(.el-form) .privacy .el-checkbox {
|
||||||
|
margin-right: 0;
|
||||||
|
height: auto;
|
||||||
|
--el-checkbox-font-size: 1.4rem;
|
||||||
|
--el-checkbox-input-width: 1.4rem;
|
||||||
|
--el-checkbox-input-height: 1.4rem;
|
||||||
|
--el-checkbox-checked-bg-color: #000;
|
||||||
|
--el-checkbox-checked-input-border-color: #000;
|
||||||
|
--el-checkbox-input-border: 0.1rem solid #c4c4c4;
|
||||||
|
--el-checkbox-bg-color: #fff;
|
||||||
|
--el-checkbox-border-radius: 0;
|
||||||
|
}
|
||||||
|
.retrieve-password:deep(.el-form) .privacy .el-checkbox .el-checkbox__input.is-indeterminate .el-checkbox__inner:before,
|
||||||
|
.register:deep(.el-form) .privacy .el-checkbox .el-checkbox__input.is-indeterminate .el-checkbox__inner:before,
|
||||||
|
.login:deep(.el-form) .privacy .el-checkbox .el-checkbox__input.is-indeterminate .el-checkbox__inner:before {
|
||||||
|
height: 0.2rem;
|
||||||
|
width: 65%;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: scale(1) translate(-50%, -50%);
|
||||||
|
border-radius: 0.1rem;
|
||||||
|
}
|
||||||
|
.retrieve-password:deep(.el-form) .privacy .el-checkbox .el-checkbox__inner:after,
|
||||||
|
.register:deep(.el-form) .privacy .el-checkbox .el-checkbox__inner:after,
|
||||||
|
.login:deep(.el-form) .privacy .el-checkbox .el-checkbox__inner:after {
|
||||||
|
width: 0.4rem;
|
||||||
|
height: 0.8rem;
|
||||||
|
border-width: 0.1rem;
|
||||||
|
}
|
||||||
|
.retrieve-password:deep(.el-form) .privacy .el-checkbox .el-checkbox__label,
|
||||||
|
.register:deep(.el-form) .privacy .el-checkbox .el-checkbox__label,
|
||||||
|
.login:deep(.el-form) .privacy .el-checkbox .el-checkbox__label {
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
color: #666666;
|
color: #666666;
|
||||||
font-family: KaiseiOpti-Regular;
|
font-family: KaiseiOpti-Regular;
|
||||||
}
|
}
|
||||||
.retrieve-password:deep(.el-form) .privacy .el-checkbox__label > div > span,
|
.retrieve-password:deep(.el-form) .privacy .el-checkbox .el-checkbox__label > div > span,
|
||||||
.register:deep(.el-form) .privacy .el-checkbox__label > div > span,
|
.register:deep(.el-form) .privacy .el-checkbox .el-checkbox__label > div > span,
|
||||||
.login:deep(.el-form) .privacy .el-checkbox__label > div > span {
|
.login:deep(.el-form) .privacy .el-checkbox .el-checkbox__label > div > span {
|
||||||
font-family: KaiseiOpti-Bold;
|
font-family: KaiseiOpti-Bold;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|||||||
@@ -29,6 +29,7 @@
|
|||||||
|
|
||||||
.el-input {
|
.el-input {
|
||||||
--el-input-height: 3.4rem;
|
--el-input-height: 3.4rem;
|
||||||
|
--el-input-inner-height: auto;
|
||||||
--el-input-border-radius: 0;
|
--el-input-border-radius: 0;
|
||||||
--el-input-text-color: #232323;
|
--el-input-text-color: #232323;
|
||||||
--el-border-color: #C4C4C4;
|
--el-border-color: #C4C4C4;
|
||||||
@@ -37,6 +38,10 @@
|
|||||||
&::placeholder {
|
&::placeholder {
|
||||||
color: #9F9F9F;
|
color: #9F9F9F;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.el-input__wrapper {
|
||||||
|
padding: 0.1rem 1rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.password-tip {
|
.password-tip {
|
||||||
@@ -95,22 +100,53 @@
|
|||||||
margin-top: -0.6rem;
|
margin-top: -0.6rem;
|
||||||
--el-checkbox-height: auto;
|
--el-checkbox-height: auto;
|
||||||
|
|
||||||
.el-checkbox__label {
|
|
||||||
font-size: 1.1rem;
|
|
||||||
color: #666666;
|
|
||||||
font-family: KaiseiOpti-Regular;
|
|
||||||
|
|
||||||
>div {
|
|
||||||
>span {
|
.el-checkbox {
|
||||||
font-family: KaiseiOpti-Bold;
|
margin-right: 0;
|
||||||
text-decoration: underline;
|
height: auto;
|
||||||
cursor: pointer;
|
--el-checkbox-font-size: 1.4rem;
|
||||||
color: #232323;
|
--el-checkbox-input-width: 1.4rem;
|
||||||
|
--el-checkbox-input-height: 1.4rem;
|
||||||
|
--el-checkbox-checked-bg-color: #000;
|
||||||
|
--el-checkbox-checked-input-border-color: #000;
|
||||||
|
--el-checkbox-input-border: 0.1rem solid #c4c4c4;
|
||||||
|
--el-checkbox-bg-color: #fff;
|
||||||
|
--el-checkbox-border-radius: 0;
|
||||||
|
|
||||||
|
.el-checkbox__input.is-indeterminate .el-checkbox__inner:before {
|
||||||
|
height: 0.2rem;
|
||||||
|
width: 65%;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: scale(1) translate(-50%, -50%);
|
||||||
|
border-radius: 0.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-checkbox__inner:after {
|
||||||
|
width: 0.4rem;
|
||||||
|
height: 0.8rem;
|
||||||
|
border-width: 0.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-checkbox__label {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
color: #666666;
|
||||||
|
font-family: KaiseiOpti-Regular;
|
||||||
|
|
||||||
|
>div {
|
||||||
|
>span {
|
||||||
|
font-family: KaiseiOpti-Bold;
|
||||||
|
text-decoration: underline;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #232323;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
>.other-login {
|
>.other-login {
|
||||||
|
|||||||
@@ -8,7 +8,9 @@
|
|||||||
v-for="v in navList1"
|
v-for="v in navList1"
|
||||||
:key="v.path"
|
:key="v.path"
|
||||||
class="nav-item"
|
class="nav-item"
|
||||||
:class="{ active: activePath === v.path }"
|
:class="{
|
||||||
|
active: v.path === '/' ? activePath === v.path : new RegExp(`^${v.path}`).test(activePath)
|
||||||
|
}"
|
||||||
@click="onNavItemClick(v.path)"
|
@click="onNavItemClick(v.path)"
|
||||||
>
|
>
|
||||||
<span>{{ v.name }}</span>
|
<span>{{ v.name }}</span>
|
||||||
@@ -19,7 +21,7 @@
|
|||||||
class="icon"
|
class="icon"
|
||||||
v-for="v in navList2"
|
v-for="v in navList2"
|
||||||
:key="v.path"
|
:key="v.path"
|
||||||
:class="{ active: activePath === v.path }"
|
:class="{ active: new RegExp(`^${v.path}`).test(activePath) }"
|
||||||
@click="onNavItemClick(v.path)"
|
@click="onNavItemClick(v.path)"
|
||||||
>
|
>
|
||||||
<svg-icon :name="activePath === v.path ? v.active_icon : v.icon" size="22" />
|
<svg-icon :name="activePath === v.path ? v.active_icon : v.icon" size="22" />
|
||||||
@@ -62,6 +64,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-popover>
|
</el-popover>
|
||||||
|
<div class="language" @click="onLanguageClick">
|
||||||
|
<span :class="{ active: locale === 'CHINESE_SIMPLIFIED' }">中</span> /
|
||||||
|
<span :class="{ active: locale === 'ENGLISH' }">ENG</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -70,6 +76,8 @@
|
|||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { useRouter, useRoute } from 'vue-router'
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
import myEvent from '@/utils/myEvent'
|
import myEvent from '@/utils/myEvent'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
const { t, locale } = useI18n()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const activePath = computed(() => route.path)
|
const activePath = computed(() => route.path)
|
||||||
@@ -95,12 +103,12 @@
|
|||||||
{
|
{
|
||||||
icon: 'cart_0',
|
icon: 'cart_0',
|
||||||
active_icon: 'cart_1',
|
active_icon: 'cart_1',
|
||||||
path: '/cart'
|
path: '/shoppingCart'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: 'user_0',
|
icon: 'user_0',
|
||||||
active_icon: 'user_1',
|
active_icon: 'user_1',
|
||||||
path: '/user'
|
path: '/account'
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
const onNavItemClick = (path: string) => {
|
const onNavItemClick = (path: string) => {
|
||||||
@@ -117,10 +125,11 @@
|
|||||||
const onMyWardrobe = () => {
|
const onMyWardrobe = () => {
|
||||||
hideProfilePopover()
|
hideProfilePopover()
|
||||||
console.log('my wardrobe')
|
console.log('my wardrobe')
|
||||||
|
router.push('/wardrobe')
|
||||||
}
|
}
|
||||||
const onNotifications = () => {
|
const onNotifications = () => {
|
||||||
hideProfilePopover()
|
hideProfilePopover()
|
||||||
console.log('notifications')
|
router.push('/notifications')
|
||||||
}
|
}
|
||||||
const onSettings = () => {
|
const onSettings = () => {
|
||||||
hideProfilePopover()
|
hideProfilePopover()
|
||||||
@@ -130,9 +139,13 @@
|
|||||||
hideProfilePopover()
|
hideProfilePopover()
|
||||||
console.log('logout')
|
console.log('logout')
|
||||||
}
|
}
|
||||||
|
const onLanguageClick = () => {
|
||||||
|
locale.value = locale.value === 'ENGLISH' ? 'CHINESE_SIMPLIFIED' : 'ENGLISH'
|
||||||
|
localStorage.setItem('language', locale.value)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less">
|
<style lang="less" scoped>
|
||||||
#main-header {
|
#main-header {
|
||||||
height: var(--header-height);
|
height: var(--header-height);
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -158,7 +171,6 @@
|
|||||||
height: 2.4rem;
|
height: 2.4rem;
|
||||||
}
|
}
|
||||||
> .login {
|
> .login {
|
||||||
font-family: Kaisei Opti;
|
|
||||||
font-size: 1.6rem;
|
font-size: 1.6rem;
|
||||||
}
|
}
|
||||||
> .profile {
|
> .profile {
|
||||||
@@ -167,6 +179,15 @@
|
|||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: #f5f5f5;
|
background: #f5f5f5;
|
||||||
}
|
}
|
||||||
|
> .language {
|
||||||
|
font-family: KaiseiOpti-Regular;
|
||||||
|
font-size: 1.6rem;
|
||||||
|
color: #c2c2c2;
|
||||||
|
> .active {
|
||||||
|
color: #232323;
|
||||||
|
font-family: KaiseiOpti-Medium;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
> .center,
|
> .center,
|
||||||
> .right {
|
> .right {
|
||||||
@@ -194,7 +215,6 @@
|
|||||||
font-size: 1.6rem;
|
font-size: 1.6rem;
|
||||||
color: #232323;
|
color: #232323;
|
||||||
border-bottom: 0.1rem solid transparent;
|
border-bottom: 0.1rem solid transparent;
|
||||||
font-family: Kaisei Opti;
|
|
||||||
}
|
}
|
||||||
&.active {
|
&.active {
|
||||||
> span {
|
> span {
|
||||||
|
|||||||
136
src/views/notifications/components/NotificationListItem.vue
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
<template>
|
||||||
|
<article class="notification-item" :class="{ expanded: item.isExpanded, unread: item.isUnread }">
|
||||||
|
<button type="button" class="notification-trigger" @click="handleToggle">
|
||||||
|
<span class="status-dot" :class="{ visible: item.isUnread }" />
|
||||||
|
|
||||||
|
<div class="notification-main">
|
||||||
|
<h3 class="notification-title">{{ item.title }}</h3>
|
||||||
|
<p class="notification-date">{{ item.date }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span class="notification-arrow" :class="arrowClasses" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div v-if="item.isExpanded" class="notification-content">
|
||||||
|
{{ item.content }}
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import type { NotificationRecord } from '../types'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
item: NotificationRecord
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
toggle: [id: NotificationRecord['id']]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const arrowClasses = computed(() => ({
|
||||||
|
expanded: props.item.isExpanded
|
||||||
|
}))
|
||||||
|
|
||||||
|
const handleToggle = () => {
|
||||||
|
emit('toggle', props.item.id)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.notification-item {
|
||||||
|
border-bottom: 0.05rem solid #ded8d2;
|
||||||
|
|
||||||
|
&.expanded {
|
||||||
|
.notification-title {
|
||||||
|
color: #585858;
|
||||||
|
font-family: 'KaiseiOpti-Medium';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-trigger {
|
||||||
|
width: 100%;
|
||||||
|
padding: 2rem 0;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 0.8rem minmax(0, 1fr) 2.4rem;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2.4rem;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot {
|
||||||
|
width: 0.8rem;
|
||||||
|
height: 0.8rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: transparent;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
|
||||||
|
&.visible {
|
||||||
|
background: #232323;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-main {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-title {
|
||||||
|
margin: 0;
|
||||||
|
color: #232323;
|
||||||
|
font-size: 1.6rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
font-family: 'KaiseiOpti-Bold';
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-date {
|
||||||
|
margin: 0.8rem 0 0;
|
||||||
|
color: #9f9f9f;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
font-family: 'KaiseiOpti-Regular';
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-arrow {
|
||||||
|
width: 0.9rem;
|
||||||
|
height: 0.9rem;
|
||||||
|
justify-self: end;
|
||||||
|
border-right: 0.1rem solid #8f8f8f;
|
||||||
|
border-bottom: 0.1rem solid #8f8f8f;
|
||||||
|
transform: rotate(-45deg);
|
||||||
|
transition: transform 0.2s ease, border-color 0.2s ease;
|
||||||
|
|
||||||
|
&.expanded {
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-content {
|
||||||
|
padding: 0 5.6rem 2.4rem 3.2rem;
|
||||||
|
color: #585858;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
line-height: 2;
|
||||||
|
font-family: 'KaiseiOpti-Regular';
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.notification-trigger {
|
||||||
|
gap: 1.6rem;
|
||||||
|
padding: 1.8rem 0;
|
||||||
|
grid-template-columns: 0.8rem minmax(0, 1fr) 1.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-content {
|
||||||
|
padding: 0 3rem 2rem 2.4rem;
|
||||||
|
font-size: 1.3rem;
|
||||||
|
line-height: 1.8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
111
src/views/notifications/components/NotificationsList.vue
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
<template>
|
||||||
|
<section class="notifications-panel">
|
||||||
|
<div class="notifications-toolbar">
|
||||||
|
<div class="unread-summary">
|
||||||
|
<span class="unread-label">UNREAD</span>
|
||||||
|
<span class="unread-count">{{ unreadCount }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="mark-all-button"
|
||||||
|
:disabled="unreadCount === 0"
|
||||||
|
@click="emit('markAllAsRead')"
|
||||||
|
>
|
||||||
|
Mark all as read
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="notifications-items">
|
||||||
|
<NotificationListItem
|
||||||
|
v-for="item in items"
|
||||||
|
:key="item.id"
|
||||||
|
:item="item"
|
||||||
|
@toggle="emit('toggleItem', $event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import NotificationListItem from './NotificationListItem.vue'
|
||||||
|
import type { NotificationRecord } from '../types'
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
items: NotificationRecord[]
|
||||||
|
unreadCount: number
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
toggleItem: [id: NotificationRecord['id']]
|
||||||
|
markAllAsRead: []
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.notifications-panel {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notifications-toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 2rem;
|
||||||
|
margin-bottom: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unread-summary {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unread-label {
|
||||||
|
color: #979797;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
font-family: 'KaiseiOpti-Regular';
|
||||||
|
}
|
||||||
|
|
||||||
|
.unread-count {
|
||||||
|
min-width: 3.1rem;
|
||||||
|
height: 2.4rem;
|
||||||
|
padding: 0 0.8rem;
|
||||||
|
border-radius: 2rem;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: #232323;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
line-height: 1;
|
||||||
|
font-family: 'KaiseiOpti-Bold';
|
||||||
|
}
|
||||||
|
|
||||||
|
.mark-all-button {
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: #979797;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
text-decoration: underline;
|
||||||
|
text-underline-offset: 0.2rem;
|
||||||
|
font-family: 'KaiseiOpti-Regular';
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
cursor: default;
|
||||||
|
opacity: 0.55;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.notifications-toolbar {
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-bottom: 2.8rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
166
src/views/notifications/index.vue
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
<template>
|
||||||
|
<div class="notifications-view mini-scrollbar">
|
||||||
|
<section class="notifications-hero">
|
||||||
|
<div class="notifications-hero__content">
|
||||||
|
<h1 class="notifications-hero__title">Notifications</h1>
|
||||||
|
<p class="notifications-hero__subtitle">System announcements and updates</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="notifications-content">
|
||||||
|
<NotificationsList
|
||||||
|
:items="notifications"
|
||||||
|
:unread-count="unreadCount"
|
||||||
|
@toggle-item="handleToggleItem"
|
||||||
|
@mark-all-as-read="handleMarkAllAsRead"
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
import NotificationsList from './components/NotificationsList.vue'
|
||||||
|
import type { NotificationRecord } from './types'
|
||||||
|
|
||||||
|
const notifications = ref<NotificationRecord[]>([
|
||||||
|
{
|
||||||
|
id: 'maintenance-mar-10-primary',
|
||||||
|
title: 'Platform Maintenance Notice - Mar 10, 2026',
|
||||||
|
date: 'Mar 6, 2026',
|
||||||
|
content:
|
||||||
|
'We will perform scheduled platform maintenance on Mar 10, 2026 to improve checkout stability and notification delivery. During this window, a few account features may respond more slowly than usual.',
|
||||||
|
isUnread: true,
|
||||||
|
isExpanded: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'maintenance-mar-10-reminder',
|
||||||
|
title: 'Platform Maintenance Notice - Mar 10, 2026',
|
||||||
|
date: 'Feb 28, 2026',
|
||||||
|
content:
|
||||||
|
'This is an early reminder for the Mar 10 maintenance window. Please avoid making urgent profile or order updates right before the scheduled service period.',
|
||||||
|
isUnread: true,
|
||||||
|
isExpanded: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'terms-mar-1',
|
||||||
|
title: 'Updated Terms of Service - Effective Mar 1, 2026',
|
||||||
|
date: 'Feb 20, 2026',
|
||||||
|
content:
|
||||||
|
'We updated our Terms of Service to clarify digital item ownership, payment processing responsibilities, and account conduct expectations. Please review the new terms before your next purchase.',
|
||||||
|
isUnread: true,
|
||||||
|
isExpanded: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'welcome',
|
||||||
|
title: 'Welcome to Stylish Parade',
|
||||||
|
date: 'Jan 4, 2026',
|
||||||
|
content:
|
||||||
|
'Thanks for joining Stylish Parade. Explore brand stories, save your favorite pieces, and keep your profile updated so we can recommend the right collections for you.',
|
||||||
|
isUnread: false,
|
||||||
|
isExpanded: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'holiday-support',
|
||||||
|
title: 'Platform Maintenance Notice - Mar 10, 2026',
|
||||||
|
date: 'Dec 20, 2025',
|
||||||
|
content:
|
||||||
|
'Our customer support team will have limited availability during the holiday season from Dec 24 to Jan 2. Response times may be longer than usual. The platform will remain fully operational throughout this period. We wish you a wonderful holiday season.',
|
||||||
|
isUnread: false,
|
||||||
|
isExpanded: true
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
const unreadCount = computed(() => notifications.value.filter((item) => item.isUnread).length)
|
||||||
|
|
||||||
|
const handleToggleItem = (id: NotificationRecord['id']) => {
|
||||||
|
const targetItem = notifications.value.find((item) => item.id === id)
|
||||||
|
|
||||||
|
if (!targetItem) return
|
||||||
|
|
||||||
|
const nextExpanded = !targetItem.isExpanded
|
||||||
|
|
||||||
|
notifications.value = notifications.value.map((item) => ({
|
||||||
|
...item,
|
||||||
|
isUnread: item.id === id ? false : item.isUnread,
|
||||||
|
isExpanded: item.id === id ? nextExpanded : false
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMarkAllAsRead = () => {
|
||||||
|
notifications.value = notifications.value.map((item) => ({
|
||||||
|
...item,
|
||||||
|
isUnread: false
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.notifications-view {
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notifications-hero {
|
||||||
|
min-height: 14.8rem;
|
||||||
|
padding: 3.2rem 2rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: linear-gradient(180deg, rgba(245, 243, 240, 0.92) 0%, rgba(250, 249, 246, 0.95) 100%),
|
||||||
|
linear-gradient(
|
||||||
|
90deg,
|
||||||
|
rgba(227, 221, 212, 0.28) 0%,
|
||||||
|
rgba(255, 255, 255, 0.24) 50%,
|
||||||
|
rgba(227, 221, 212, 0.28) 100%
|
||||||
|
);
|
||||||
|
border-bottom: 0.05rem solid #dfd8d1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notifications-hero__content {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notifications-hero__title {
|
||||||
|
margin: 0;
|
||||||
|
color: #232323;
|
||||||
|
font-size: 4rem;
|
||||||
|
line-height: 1;
|
||||||
|
font-family: 'KaiseiOpti-Bold';
|
||||||
|
}
|
||||||
|
|
||||||
|
.notifications-hero__subtitle {
|
||||||
|
margin: 1.2rem 0 0;
|
||||||
|
color: #585858;
|
||||||
|
font-size: 1.6rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
font-family: 'KaiseiOpti-Regular';
|
||||||
|
}
|
||||||
|
|
||||||
|
.notifications-content {
|
||||||
|
width: min(108rem, calc(100% - 4rem));
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 4rem 0 8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.notifications-hero {
|
||||||
|
min-height: 12rem;
|
||||||
|
padding: 2.8rem 1.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notifications-hero__title {
|
||||||
|
font-size: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notifications-hero__subtitle {
|
||||||
|
margin-top: 0.8rem;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notifications-content {
|
||||||
|
width: calc(100% - 3.2rem);
|
||||||
|
padding: 3.2rem 0 4.8rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
8
src/views/notifications/types.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export interface NotificationRecord {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
date: string
|
||||||
|
content: string
|
||||||
|
isUnread: boolean
|
||||||
|
isExpanded: boolean
|
||||||
|
}
|
||||||
263
src/views/setting/components/EmailVerificationDialog.vue
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="visible" class="verification-dialog" @click.self="handleClose">
|
||||||
|
<div class="verification-dialog__panel">
|
||||||
|
<button type="button" class="verification-dialog__close" @click="handleClose">
|
||||||
|
<SvgIcon name="close" size="24" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="verification-dialog__title">{{ t('Settings.dialog.title') }}</div>
|
||||||
|
<div class="verification-dialog__subtitle">{{ t('Settings.dialog.subtitle') }}</div>
|
||||||
|
<div class="verification-dialog__email">{{ email }}</div>
|
||||||
|
|
||||||
|
<InputCode
|
||||||
|
ref="verificationCodeRef"
|
||||||
|
class="verification-code"
|
||||||
|
@update:model-value="handleVerificationCodeChange"
|
||||||
|
@submit="handleVerificationCodeChange"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="verification-dialog__submit"
|
||||||
|
:disabled="saving || verificationCode.length !== 6"
|
||||||
|
@click="handleSubmit"
|
||||||
|
>
|
||||||
|
{{ t('Settings.dialog.submit') }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="verification-dialog__resend"
|
||||||
|
:disabled="resendCountdown > 0"
|
||||||
|
@click="handleResend"
|
||||||
|
>
|
||||||
|
{{ resendButtonText }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import InputCode from '@/components/input-code.vue'
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
visible: boolean
|
||||||
|
email: string
|
||||||
|
saving?: boolean
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
saving: false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
close: []
|
||||||
|
resend: []
|
||||||
|
submit: [code: string]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const RESEND_COUNTDOWN_SECONDS = 60
|
||||||
|
|
||||||
|
const verificationCode = ref('')
|
||||||
|
const resendCountdown = ref(RESEND_COUNTDOWN_SECONDS)
|
||||||
|
const verificationCodeRef = ref<{ resetCode: (size?: number) => void } | null>(null)
|
||||||
|
let resendCountdownTimer: number | null = null
|
||||||
|
|
||||||
|
const formattedResendCountdown = computed(
|
||||||
|
() => `00:${String(resendCountdown.value).padStart(2, '0')}`
|
||||||
|
)
|
||||||
|
|
||||||
|
const resendButtonText = computed(() =>
|
||||||
|
resendCountdown.value > 0
|
||||||
|
? t('Settings.dialog.resendCodeIn', { time: formattedResendCountdown.value })
|
||||||
|
: t('Settings.dialog.resendCode')
|
||||||
|
)
|
||||||
|
|
||||||
|
const clearResendCountdownTimer = () => {
|
||||||
|
if (resendCountdownTimer) {
|
||||||
|
window.clearInterval(resendCountdownTimer)
|
||||||
|
resendCountdownTimer = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const startResendCountdown = () => {
|
||||||
|
clearResendCountdownTimer()
|
||||||
|
resendCountdown.value = RESEND_COUNTDOWN_SECONDS
|
||||||
|
resendCountdownTimer = window.setInterval(() => {
|
||||||
|
if (resendCountdown.value <= 1) {
|
||||||
|
resendCountdown.value = 0
|
||||||
|
clearResendCountdownTimer()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resendCountdown.value -= 1
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetVerificationCodeInput = async () => {
|
||||||
|
verificationCode.value = ''
|
||||||
|
await nextTick()
|
||||||
|
verificationCodeRef.value?.resetCode?.()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
emit('close')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleVerificationCodeChange = (value: string) => {
|
||||||
|
verificationCode.value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
if (verificationCode.value.length !== 6) return
|
||||||
|
|
||||||
|
emit('submit', verificationCode.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleResend = async () => {
|
||||||
|
if (resendCountdown.value > 0) return
|
||||||
|
|
||||||
|
emit('resend')
|
||||||
|
startResendCountdown()
|
||||||
|
await resetVerificationCodeInput()
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.visible,
|
||||||
|
async (visible) => {
|
||||||
|
if (!visible) {
|
||||||
|
clearResendCountdownTimer()
|
||||||
|
verificationCode.value = ''
|
||||||
|
resendCountdown.value = RESEND_COUNTDOWN_SECONDS
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await resetVerificationCodeInput()
|
||||||
|
startResendCountdown()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.email,
|
||||||
|
async () => {
|
||||||
|
if (!props.visible) return
|
||||||
|
|
||||||
|
await resetVerificationCodeInput()
|
||||||
|
startResendCountdown()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
clearResendCountdownTimer()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.verification-dialog {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 2000;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 2rem;
|
||||||
|
background: rgba(0, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.verification-dialog__panel {
|
||||||
|
width: 60rem;
|
||||||
|
padding: 4.8rem 7.2rem 5rem;
|
||||||
|
position: relative;
|
||||||
|
background: #efefef;
|
||||||
|
box-shadow: 0 2rem 6rem rgba(35, 35, 35, 0.14);
|
||||||
|
}
|
||||||
|
|
||||||
|
.verification-dialog__close {
|
||||||
|
position: absolute;
|
||||||
|
top: 2rem;
|
||||||
|
right: 2rem;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: transparent;
|
||||||
|
color: #585858;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.verification-dialog__title {
|
||||||
|
color: #232323;
|
||||||
|
text-align: center;
|
||||||
|
font-family: 'KaiseiOpti-Bold';
|
||||||
|
font-size: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.verification-dialog__subtitle {
|
||||||
|
margin-top: 2rem;
|
||||||
|
color: #585858;
|
||||||
|
text-align: center;
|
||||||
|
font-family: 'KaiseiOpti-Regular';
|
||||||
|
font-size: 1.6rem;
|
||||||
|
line-height: 2.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.verification-dialog__email {
|
||||||
|
margin-top: 0.8rem;
|
||||||
|
color: #232323;
|
||||||
|
text-align: center;
|
||||||
|
font-family: 'KaiseiOpti-Bold';
|
||||||
|
font-size: 1.6rem;
|
||||||
|
line-height: 2.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.verification-code {
|
||||||
|
margin-top: 4.8rem;
|
||||||
|
--input-code-justify-content: center;
|
||||||
|
--input-code-input-gap: 1.4rem;
|
||||||
|
--input-code-input-width: 6rem;
|
||||||
|
--input-code-input-height: 6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.verification-dialog__submit {
|
||||||
|
width: 100%;
|
||||||
|
height: 4.8rem;
|
||||||
|
margin-top: 5rem;
|
||||||
|
border: 0.1rem solid #232323;
|
||||||
|
background: #232323;
|
||||||
|
color: #ffffff;
|
||||||
|
font-family: 'KaiseiOpti-Bold';
|
||||||
|
font-size: 1.6rem;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.verification-dialog__resend {
|
||||||
|
margin: 2rem auto 0;
|
||||||
|
display: block;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: #7c7c7c;
|
||||||
|
font-family: 'KaiseiOpti-Regular';
|
||||||
|
font-size: 1.6rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
text-decoration: underline;
|
||||||
|
text-underline-offset: 0.2rem;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
cursor: default;
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
235
src/views/setting/components/ProfileSection.vue
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
<template>
|
||||||
|
<SettingsSection
|
||||||
|
:title="t('Settings.profile.title')"
|
||||||
|
:description="t('Settings.profile.description')"
|
||||||
|
>
|
||||||
|
<div class="profile-header">
|
||||||
|
<div class="avatar relative">
|
||||||
|
<SvgIcon name="user_0" size="46" class="avatar-icon" />
|
||||||
|
<img src="@/assets/images/edit.png" class="avatar-edit-icon" />
|
||||||
|
</div>
|
||||||
|
<div class="profile-summary">
|
||||||
|
<div class="profile-name">{{ fullName }}</div>
|
||||||
|
<div class="profile-email">{{ displayData.email }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="read-section">
|
||||||
|
<div class="read-row two-column">
|
||||||
|
<div class="read-item">
|
||||||
|
<div class="read-label">{{ t('Settings.profile.firstName') }}</div>
|
||||||
|
<div v-show="!isEditing" class="read-box">{{ displayData.firstName }}</div>
|
||||||
|
<div v-show="isEditing" class="form-item-value name">
|
||||||
|
<el-input
|
||||||
|
:model-value="firstName"
|
||||||
|
:placeholder="t('Settings.profile.firstNamePlaceholder')"
|
||||||
|
@update:model-value="emit('update:firstName', String($event))"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="read-item">
|
||||||
|
<div class="read-label">{{ t('Settings.profile.lastName') }}</div>
|
||||||
|
<div v-show="!isEditing" class="read-box">{{ displayData.lastName }}</div>
|
||||||
|
<div v-show="isEditing" class="form-item-value name">
|
||||||
|
<el-input
|
||||||
|
:model-value="lastName"
|
||||||
|
:placeholder="t('Settings.profile.lastNamePlaceholder')"
|
||||||
|
@update:model-value="emit('update:lastName', String($event))"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="read-row">
|
||||||
|
<div class="read-label">{{ t('Settings.profile.username') }}</div>
|
||||||
|
<div v-show="!isEditing" class="read-box">{{ displayData.username }}</div>
|
||||||
|
<div v-show="isEditing" class="form-item-value">
|
||||||
|
<el-input
|
||||||
|
:model-value="username"
|
||||||
|
:placeholder="t('Settings.profile.usernamePlaceholder')"
|
||||||
|
@update:model-value="emit('update:username', String($event))"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="read-tip">{{ t('Settings.profile.usernameTip') }}</div>
|
||||||
|
|
||||||
|
<div class="read-row role-row">
|
||||||
|
<div class="read-label">{{ t('Settings.profile.role') }}</div>
|
||||||
|
<div :class="{ 'readonly-radio-group': !isEditing }">
|
||||||
|
<Radio multiple :max="2" v-model="roleSelection" :options="roleOptions" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="read-tip">{{ t('Settings.profile.roleTip') }}</div>
|
||||||
|
</div>
|
||||||
|
</SettingsSection>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import SettingsSection from './SettingsSection.vue'
|
||||||
|
import Radio from './Radio.vue'
|
||||||
|
import type { RoleOption, RoleValue, SettingsData } from '../types'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
displayData: SettingsData
|
||||||
|
firstName: string
|
||||||
|
lastName: string
|
||||||
|
username: string
|
||||||
|
fullName: string
|
||||||
|
isEditing: boolean
|
||||||
|
roleModel: RoleValue[]
|
||||||
|
roleOptions: RoleOption[]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'update:firstName', value: string): void
|
||||||
|
(event: 'update:lastName', value: string): void
|
||||||
|
(event: 'update:username', value: string): void
|
||||||
|
(event: 'update:roleModel', value: RoleValue[]): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const roleSelection = computed<RoleValue[]>({
|
||||||
|
get: () => props.roleModel,
|
||||||
|
set: (value) => {
|
||||||
|
emit('update:roleModel', value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.field-text() {
|
||||||
|
font-family: 'KaiseiOpti-Regular';
|
||||||
|
font-size: 1.6rem;
|
||||||
|
line-height: 2.4rem;
|
||||||
|
color: #232323;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-frame() {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 4rem;
|
||||||
|
border: 0.1rem solid #979797;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-wrapper() {
|
||||||
|
box-shadow: none;
|
||||||
|
border-radius: 0;
|
||||||
|
padding: 0 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
column-gap: 2.6rem;
|
||||||
|
margin-bottom: 3.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
width: 8rem;
|
||||||
|
height: 8rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 0.1rem solid #d8d0c7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-edit-icon {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 3rem;
|
||||||
|
height: 3rem;
|
||||||
|
border: 0.1rem solid #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-summary {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
row-gap: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-name {
|
||||||
|
font-family: 'KaiseiOpti-Medium';
|
||||||
|
font-size: 2.4rem;
|
||||||
|
line-height: 3.6rem;
|
||||||
|
color: #232323;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-email {
|
||||||
|
font-family: 'KaiseiOpti-Regular';
|
||||||
|
font-size: 1.8rem;
|
||||||
|
line-height: 2.4rem;
|
||||||
|
color: #979797;
|
||||||
|
}
|
||||||
|
|
||||||
|
.read-section {
|
||||||
|
width: 58rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.read-label {
|
||||||
|
margin-bottom: 0.8rem;
|
||||||
|
font-family: 'KaiseiOpti-Medium';
|
||||||
|
font-size: 1.4rem;
|
||||||
|
line-height: 2.4rem;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
color: #585858;
|
||||||
|
}
|
||||||
|
|
||||||
|
.read-tip {
|
||||||
|
margin-top: 0.8rem;
|
||||||
|
font-family: 'KaiseiOpti-Regular';
|
||||||
|
font-size: 1.2rem;
|
||||||
|
line-height: 1.6rem;
|
||||||
|
color: #9f9f9f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.read-row + .read-row {
|
||||||
|
margin-top: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.read-row.two-column {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 28.4rem 28.4rem;
|
||||||
|
column-gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.read-box {
|
||||||
|
.field-frame();
|
||||||
|
.field-text();
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.8rem 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-item-value {
|
||||||
|
.field-frame();
|
||||||
|
|
||||||
|
&.name {
|
||||||
|
width: 28.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-input) {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-input__wrapper) {
|
||||||
|
.control-wrapper();
|
||||||
|
min-height: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-input__inner) {
|
||||||
|
.field-text();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.readonly-radio-group {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-row {
|
||||||
|
margin-top: 3rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
<div class="radio-button-group">
|
<div class="radio-button-group">
|
||||||
<button
|
<button
|
||||||
v-for="item in options"
|
v-for="item in options"
|
||||||
:key="item.value"
|
:key="String(item.value)"
|
||||||
type="button"
|
type="button"
|
||||||
:class="[
|
:class="[
|
||||||
'radio-button',
|
'radio-button',
|
||||||
|
|||||||
128
src/views/setting/components/RegionSection.vue
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
<template>
|
||||||
|
<SettingsSection
|
||||||
|
:title="t('Settings.region.title')"
|
||||||
|
:description="t('Settings.region.description')"
|
||||||
|
>
|
||||||
|
<div class="region-row">
|
||||||
|
<div class="security-label">{{ t('Settings.region.displayLanguage') }}</div>
|
||||||
|
<div v-show="!isEditing" class="security-static field-box">
|
||||||
|
{{ displayLanguageLabel }}
|
||||||
|
</div>
|
||||||
|
<div v-show="isEditing" class="outlined-field select-field">
|
||||||
|
<el-select
|
||||||
|
:model-value="language"
|
||||||
|
:placeholder="t('Settings.region.selectLanguage')"
|
||||||
|
@update:model-value="emit('update:language', $event as LanguageValue)"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in languageOptions"
|
||||||
|
:key="item.value"
|
||||||
|
:label="item.label"
|
||||||
|
:value="item.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="region-row">
|
||||||
|
<div class="security-label">{{ t('Settings.region.region') }}</div>
|
||||||
|
<div v-show="!isEditing" class="security-static field-box">
|
||||||
|
{{ displayRegionLabel }}
|
||||||
|
</div>
|
||||||
|
<div v-show="isEditing" class="outlined-field select-field">
|
||||||
|
<el-select
|
||||||
|
:model-value="region"
|
||||||
|
:placeholder="t('Settings.region.selectRegion')"
|
||||||
|
@update:model-value="emit('update:region', $event as RegionValue)"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in regionOptions"
|
||||||
|
:key="item.value"
|
||||||
|
:label="item.label"
|
||||||
|
:value="item.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SettingsSection>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import SettingsSection from './SettingsSection.vue'
|
||||||
|
import type { LanguageValue, RegionValue, SettingOption } from '../types'
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
language: LanguageValue
|
||||||
|
region: RegionValue
|
||||||
|
displayLanguageLabel: string
|
||||||
|
displayRegionLabel: string
|
||||||
|
isEditing: boolean
|
||||||
|
languageOptions: SettingOption<LanguageValue>[]
|
||||||
|
regionOptions: SettingOption<RegionValue>[]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'update:language', value: LanguageValue): void
|
||||||
|
(event: 'update:region', value: RegionValue): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.field-text() {
|
||||||
|
font-family: 'KaiseiOpti-Regular';
|
||||||
|
font-size: 1.6rem;
|
||||||
|
line-height: 2.4rem;
|
||||||
|
color: #232323;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-frame() {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 4rem;
|
||||||
|
border: 0.1rem solid #979797;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-wrapper() {
|
||||||
|
box-shadow: none;
|
||||||
|
border-radius: 0;
|
||||||
|
padding: 0 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.region-row + .region-row {
|
||||||
|
margin-top: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.security-label {
|
||||||
|
margin-bottom: 0.8rem;
|
||||||
|
font-family: 'KaiseiOpti-Medium';
|
||||||
|
font-size: 1.4rem;
|
||||||
|
line-height: 2.4rem;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
color: #585858;
|
||||||
|
}
|
||||||
|
|
||||||
|
.security-static,
|
||||||
|
.field-box {
|
||||||
|
.field-frame();
|
||||||
|
.field-text();
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.8rem 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.outlined-field {
|
||||||
|
.field-frame();
|
||||||
|
|
||||||
|
:deep(.el-select) {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-select__wrapper) {
|
||||||
|
.control-wrapper();
|
||||||
|
min-height: 4rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
242
src/views/setting/components/SecuritySection.vue
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
<template>
|
||||||
|
<SettingsSection
|
||||||
|
:title="t('Settings.security.title')"
|
||||||
|
:description="t('Settings.security.description')"
|
||||||
|
content-class="security-container"
|
||||||
|
>
|
||||||
|
<div class="inner-divider" />
|
||||||
|
<div class="security-row">
|
||||||
|
<div class="security-inline-row">
|
||||||
|
<div class="security-label inline">{{ t('Settings.security.email') }}</div>
|
||||||
|
<div class="security-static">{{ email }}</div>
|
||||||
|
<button v-show="isEditing" type="button" class="small-btn" @click="emit('reset-email')">
|
||||||
|
{{ t('Settings.buttons.cancel') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-show="isEditing" class="security-row">
|
||||||
|
<div class="security-label">{{ t('Settings.security.newEmail') }}</div>
|
||||||
|
<div class="outlined-field verify-field">
|
||||||
|
<el-input
|
||||||
|
:model-value="newEmail"
|
||||||
|
:placeholder="t('Settings.security.newEmailPlaceholder')"
|
||||||
|
@update:model-value="emit('update:newEmail', String($event))"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="verify-btn"
|
||||||
|
:class="{ verified: isEmailVerified }"
|
||||||
|
@click="emit('verify-email')"
|
||||||
|
>
|
||||||
|
{{ isEmailVerified ? t('Settings.security.verified') : t('Settings.security.verify') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div v-if="isEmailVerified" class="security-tip verified-tip">
|
||||||
|
{{ t('Settings.security.verifiedTip') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="inner-divider" />
|
||||||
|
|
||||||
|
<div class="security-row">
|
||||||
|
<div class="security-inline-row">
|
||||||
|
<div class="security-label inline">{{ t('Settings.security.password') }}</div>
|
||||||
|
<div class="security-static password-mask">.........</div>
|
||||||
|
<button v-show="isEditing" type="button" class="small-btn" @click="emit('reset-password')">
|
||||||
|
{{ t('Settings.buttons.cancel') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-show="isEditing" class="security-row">
|
||||||
|
<div class="security-label">{{ t('Settings.security.newPassword') }}</div>
|
||||||
|
<div class="outlined-field">
|
||||||
|
<el-input
|
||||||
|
:model-value="newPassword"
|
||||||
|
type="password"
|
||||||
|
show-password
|
||||||
|
:placeholder="t('Settings.security.newPasswordPlaceholder')"
|
||||||
|
@update:model-value="emit('update:newPassword', String($event))"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="security-tip">{{ t('Settings.security.passwordTip') }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-show="isEditing" class="security-row">
|
||||||
|
<div class="security-label">{{ t('Settings.security.currentPassword') }}</div>
|
||||||
|
<div class="outlined-field">
|
||||||
|
<el-input
|
||||||
|
:model-value="currentPassword"
|
||||||
|
type="password"
|
||||||
|
show-password
|
||||||
|
:placeholder="t('Settings.security.currentPasswordPlaceholder')"
|
||||||
|
@update:model-value="emit('update:currentPassword', String($event))"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="inner-divider" />
|
||||||
|
</SettingsSection>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import SettingsSection from './SettingsSection.vue'
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
email: string
|
||||||
|
newEmail: string
|
||||||
|
newPassword: string
|
||||||
|
currentPassword: string
|
||||||
|
isEditing: boolean
|
||||||
|
isEmailVerified: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'update:newEmail', value: string): void
|
||||||
|
(event: 'update:newPassword', value: string): void
|
||||||
|
(event: 'update:currentPassword', value: string): void
|
||||||
|
(event: 'reset-email'): void
|
||||||
|
(event: 'reset-password'): void
|
||||||
|
(event: 'verify-email'): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.field-text() {
|
||||||
|
font-family: 'KaiseiOpti-Regular';
|
||||||
|
font-size: 1.6rem;
|
||||||
|
line-height: 2.4rem;
|
||||||
|
color: #232323;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-frame() {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 4rem;
|
||||||
|
border: 0.1rem solid #979797;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-wrapper() {
|
||||||
|
box-shadow: none;
|
||||||
|
border-radius: 0;
|
||||||
|
padding: 0 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.security-row + .security-row {
|
||||||
|
margin-top: 2.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.security-label {
|
||||||
|
margin: 0 0 0.8rem;
|
||||||
|
font-family: 'KaiseiOpti-Medium';
|
||||||
|
font-size: 1.4rem;
|
||||||
|
line-height: 2.4rem;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
color: #585858;
|
||||||
|
|
||||||
|
&.inline {
|
||||||
|
width: 10.8rem;
|
||||||
|
margin-bottom: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.security-static {
|
||||||
|
.field-text();
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 2.4rem;
|
||||||
|
padding: 0.1rem 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.security-inline-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2.8rem;
|
||||||
|
min-height: 3.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.security-tip {
|
||||||
|
margin-top: 0.6rem;
|
||||||
|
font-family: 'KaiseiOpti-Regular';
|
||||||
|
font-size: 1.2rem;
|
||||||
|
line-height: 1.6rem;
|
||||||
|
color: #9f9f9f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.outlined-field {
|
||||||
|
.field-frame();
|
||||||
|
|
||||||
|
:deep(.el-input) {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-input__wrapper) {
|
||||||
|
.control-wrapper();
|
||||||
|
min-height: 4rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.verify-field {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 0.8rem;
|
||||||
|
|
||||||
|
:deep(.el-input) {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.verify-btn {
|
||||||
|
border: none;
|
||||||
|
min-width: 11rem;
|
||||||
|
height: 2.8rem;
|
||||||
|
line-height: 2.8rem;
|
||||||
|
border-left: 0.1rem solid #979797;
|
||||||
|
background: #ffffff;
|
||||||
|
font-family: 'KaiseiOpti-Medium';
|
||||||
|
font-size: 1.4rem;
|
||||||
|
color: #232323;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0 2rem;
|
||||||
|
|
||||||
|
&.verified {
|
||||||
|
color: #ffffff;
|
||||||
|
background: #232323;
|
||||||
|
border-left-color: #232323;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.password-mask {
|
||||||
|
font-family: 'KaiseiOpti-Bold';
|
||||||
|
letter-spacing: 0.08rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inner-divider {
|
||||||
|
height: 1px;
|
||||||
|
margin: 2rem 0;
|
||||||
|
background-color: #c4c4c4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.small-btn {
|
||||||
|
width: 10rem;
|
||||||
|
height: 3.2rem;
|
||||||
|
align-self: flex-start;
|
||||||
|
border: 0.1rem solid #c4c4c4;
|
||||||
|
background: #f6f6f6;
|
||||||
|
font-family: 'KaiseiOpti-Bold';
|
||||||
|
font-size: 1.2rem;
|
||||||
|
line-height: 2.6rem;
|
||||||
|
letter-spacing: -0.03em;
|
||||||
|
color: #232323;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.verified-tip {
|
||||||
|
color: #6f7f68;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
73
src/views/setting/components/SettingsActions.vue
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
<template>
|
||||||
|
<div class="action-container">
|
||||||
|
<template v-if="isEditing">
|
||||||
|
<button type="button" class="primary-btn" :disabled="saving" @click="emit('save')">
|
||||||
|
{{ saving ? t('Settings.buttons.saving') : t('Settings.buttons.saveChange') }}
|
||||||
|
</button>
|
||||||
|
<button type="button" class="secondary-btn" :disabled="saving" @click="emit('discard')">
|
||||||
|
{{ t('Settings.buttons.discard') }}
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<button type="button" class="primary-btn edit-btn" @click="emit('edit')">
|
||||||
|
{{ t('Settings.buttons.edit') }}
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
isEditing: boolean
|
||||||
|
saving: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'edit'): void
|
||||||
|
(event: 'save'): void
|
||||||
|
(event: 'discard'): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.action-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
column-gap: 1.2rem;
|
||||||
|
margin-top: 2.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary-btn,
|
||||||
|
.secondary-btn {
|
||||||
|
height: 4.4rem;
|
||||||
|
border: 0.1rem solid #c4c4c4;
|
||||||
|
background: #f6f6f6;
|
||||||
|
font-family: 'KaiseiOpti-Bold';
|
||||||
|
font-size: 1.6rem;
|
||||||
|
line-height: 2.6rem;
|
||||||
|
color: #232323;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary-btn {
|
||||||
|
width: 23.6rem;
|
||||||
|
padding: 0 2.4rem;
|
||||||
|
border-color: #232323;
|
||||||
|
background: #232323;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondary-btn {
|
||||||
|
width: 12rem;
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
52
src/views/setting/components/SettingsSection.vue
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
<template>
|
||||||
|
<section class="setting-section">
|
||||||
|
<div class="label-container">
|
||||||
|
<div class="label">{{ title }}</div>
|
||||||
|
<div class="label-desc">{{ description }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div :class="['context-container', contentClass]">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
defineProps<{
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
contentClass?: string
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.setting-section {
|
||||||
|
display: flex;
|
||||||
|
column-gap: 21.4rem;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-container {
|
||||||
|
width: 27.6rem;
|
||||||
|
font-family: 'KaiseiOpti-Medium';
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
font-size: 2.8rem;
|
||||||
|
line-height: 3.6rem;
|
||||||
|
color: #232323;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-desc {
|
||||||
|
width: 24rem;
|
||||||
|
margin-top: 2rem;
|
||||||
|
font-size: 1.6rem;
|
||||||
|
line-height: 2.2rem;
|
||||||
|
color: #666666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-container {
|
||||||
|
width: 58rem;
|
||||||
|
padding-top: 0.2rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,377 +1,109 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="setting-wrapper mini-scrollbar">
|
<div class="setting-wrapper mini-scrollbar">
|
||||||
<div class="banner flex flex-center flex-col">
|
<div class="banner">
|
||||||
<div class="title">Settings</div>
|
<div class="title">{{ t('Settings.title') }}</div>
|
||||||
<div class="slogan">Manage your account settings and preferences</div>
|
<div class="slogan">{{ t('Settings.slogan') }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="setting-content">
|
<div class="setting-content">
|
||||||
<div class="setting-item flex">
|
<ProfileSection
|
||||||
<div class="label-container">
|
v-model:first-name="draftData.firstName"
|
||||||
<div class="label">Profile</div>
|
v-model:last-name="draftData.lastName"
|
||||||
<div class="label-desc">
|
v-model:username="draftData.username"
|
||||||
Update your display name, avatar, social links and account security.
|
v-model:role-model="roleModel"
|
||||||
</div>
|
:display-data="displayData"
|
||||||
</div>
|
:full-name="fullName"
|
||||||
|
:is-editing="isEditing"
|
||||||
<div class="context-container">
|
:role-options="roleList"
|
||||||
<div class="profile-header flex align-center">
|
/>
|
||||||
<div class="avatar relative">
|
|
||||||
<SvgIcon name="user_0" size="46" class="avatar-icon" />
|
|
||||||
<img src="@/assets/images/edit.png" class="avatar-edit-icon" />
|
|
||||||
</div>
|
|
||||||
<div class="profile-summary flex flex-col">
|
|
||||||
<div class="profile-name">{{ fullName }}</div>
|
|
||||||
<div class="profile-email">{{ displayData.email }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="read-section">
|
|
||||||
<div class="read-row two-column">
|
|
||||||
<div class="read-item">
|
|
||||||
<div class="read-label">FIRST NAME</div>
|
|
||||||
<div v-show="!isEditing" class="read-box">{{ displayData.firstName }}</div>
|
|
||||||
<div v-show="isEditing" class="form-item-value name">
|
|
||||||
<el-input v-model="draftData.firstName" placeholder="First Name" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="read-item">
|
|
||||||
<div class="read-label">LAST NAME</div>
|
|
||||||
<div v-show="!isEditing" class="read-box">{{ displayData.lastName }}</div>
|
|
||||||
<div v-show="isEditing" class="form-item-value name">
|
|
||||||
<el-input v-model="draftData.lastName" placeholder="Last Name" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="read-row">
|
|
||||||
<div class="read-label">USERNAME</div>
|
|
||||||
<div v-show="!isEditing" class="read-box">{{ displayData.username }}</div>
|
|
||||||
<div v-show="isEditing" class="form-item-value">
|
|
||||||
<el-input v-model="draftData.username" placeholder="Username" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="read-tip">Your public username on Stylish Parade.</div>
|
|
||||||
|
|
||||||
<div class="read-row role-row">
|
|
||||||
<div class="read-label">ROLE</div>
|
|
||||||
<div :class="{ 'readonly-radio-group': !isEditing }">
|
|
||||||
<Radio multiple :max="2" v-model="roleModel" :options="roleList" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="read-tip">Select up to 2 labels that suit you.</div>
|
|
||||||
|
|
||||||
<div class="social-links read-social-links">
|
|
||||||
<div class="title">SOCIAL LINKS</div>
|
|
||||||
<div class="links-list flex flex-col">
|
|
||||||
<div
|
|
||||||
class="links-item flex align-center"
|
|
||||||
v-for="(item, index) in displayData.links"
|
|
||||||
:key="`view-${index}`"
|
|
||||||
>
|
|
||||||
<div class="link-index">Link {{ index + 1 }}</div>
|
|
||||||
<div v-show="!isEditing" class="link-href flex-1 readonly">{{ item }}</div>
|
|
||||||
<div v-show="isEditing" class="link-href flex-1">
|
|
||||||
<el-input v-model="draftData.links[index]" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
v-show="isEditing"
|
|
||||||
type="button"
|
|
||||||
class="add-link-btn"
|
|
||||||
@click="handleAddLink"
|
|
||||||
>
|
|
||||||
+
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="gap" />
|
<div class="gap" />
|
||||||
|
|
||||||
<div class="setting-item flex">
|
<SecuritySection
|
||||||
<div class="label-container">
|
v-model:new-email="securityDraft.newEmail"
|
||||||
<div class="label">Security</div>
|
v-model:new-password="securityDraft.newPassword"
|
||||||
<div class="label-desc">Manage your login email and password.</div>
|
v-model:current-password="securityDraft.currentPassword"
|
||||||
</div>
|
:email="displayData.email"
|
||||||
|
:is-editing="isEditing"
|
||||||
<div class="context-container security-container">
|
:is-email-verified="isEmailVerified"
|
||||||
<div class="inner-divider" />
|
@reset-email="resetSecurityEmail"
|
||||||
<div class="security-row">
|
@reset-password="resetSecurityPassword"
|
||||||
<div class="security-inline-row flex align-center">
|
@verify-email="handleVerifyEmail"
|
||||||
<div class="security-label inline">EMAIL</div>
|
/>
|
||||||
<div class="security-static flex-1">{{ displayData.email }}</div>
|
|
||||||
<button
|
|
||||||
v-show="isEditing"
|
|
||||||
type="button"
|
|
||||||
class="small-btn"
|
|
||||||
@click="resetSecurityEmail"
|
|
||||||
>
|
|
||||||
CANCEL
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-show="isEditing" class="security-row">
|
|
||||||
<div class="security-label">NEW EMAIL ADDRESS</div>
|
|
||||||
<div class="outlined-field verify-field align-center">
|
|
||||||
<el-input v-model="securityDraft.newEmail" placeholder="Enter new email" />
|
|
||||||
<div class="verify-btn">Verify</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="inner-divider" />
|
|
||||||
|
|
||||||
<div class="security-row">
|
|
||||||
<div class="security-inline-row flex align-center">
|
|
||||||
<div class="security-label inline">PASSWORD</div>
|
|
||||||
<div class="security-static password-mask flex-1">.........</div>
|
|
||||||
<button
|
|
||||||
v-show="isEditing"
|
|
||||||
type="button"
|
|
||||||
class="small-btn"
|
|
||||||
@click="resetSecurityPassword"
|
|
||||||
>
|
|
||||||
CANCEL
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-show="isEditing" class="security-row">
|
|
||||||
<div class="security-label">NEW PASSWORD</div>
|
|
||||||
<div class="outlined-field">
|
|
||||||
<el-input
|
|
||||||
v-model="securityDraft.newPassword"
|
|
||||||
type="password"
|
|
||||||
show-password
|
|
||||||
placeholder="Enter new password"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="security-tip">You must satisfy ALL password conditions to register.</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-show="isEditing" class="security-row">
|
|
||||||
<div class="security-label">CURRENT PASSWORD</div>
|
|
||||||
<div class="outlined-field">
|
|
||||||
<el-input
|
|
||||||
v-model="securityDraft.currentPassword"
|
|
||||||
type="password"
|
|
||||||
show-password
|
|
||||||
placeholder="Confirm with your password"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="inner-divider" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="gap" />
|
<div class="gap" />
|
||||||
|
|
||||||
<div class="setting-item flex">
|
<RegionSection
|
||||||
<div class="label-container">
|
v-model:language="draftData.language"
|
||||||
<div class="label">Language & Region</div>
|
v-model:region="draftData.region"
|
||||||
<div class="label-desc">Set your preferred language, region and currency display.</div>
|
:display-language-label="displayLanguageLabel"
|
||||||
</div>
|
:display-region-label="displayRegionLabel"
|
||||||
|
:is-editing="isEditing"
|
||||||
<div class="context-container region-container">
|
:language-options="languageList"
|
||||||
<div class="region-row">
|
:region-options="regionList"
|
||||||
<div class="security-label">DISPLAY LANGUAGE</div>
|
/>
|
||||||
<div v-show="!isEditing" class="security-static field-box">
|
|
||||||
{{ displayData.language }}
|
|
||||||
</div>
|
|
||||||
<div v-show="isEditing" class="outlined-field select-field">
|
|
||||||
<el-select v-model="draftData.language" placeholder="Select language">
|
|
||||||
<el-option v-for="item in languageList" :key="item" :label="item" :value="item" />
|
|
||||||
</el-select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="region-row">
|
|
||||||
<div class="security-label">REGION</div>
|
|
||||||
<div v-show="!isEditing" class="security-static field-box">
|
|
||||||
{{ displayData.region }}
|
|
||||||
</div>
|
|
||||||
<div v-show="isEditing" class="outlined-field select-field">
|
|
||||||
<el-select v-model="draftData.region" placeholder="Select region">
|
|
||||||
<el-option v-for="item in regionList" :key="item" :label="item" :value="item" />
|
|
||||||
</el-select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="gap bottom-gap" />
|
<div class="gap bottom-gap" />
|
||||||
|
|
||||||
<div class="action-container flex">
|
<SettingsActions
|
||||||
<template v-if="isEditing">
|
:is-editing="isEditing"
|
||||||
<button type="button" class="primary-btn" :disabled="saving" @click="handleSave">
|
:saving="saving"
|
||||||
{{ saving ? 'SAVING...' : 'SAVE CHANGE' }}
|
@edit="handleEdit"
|
||||||
</button>
|
@save="handleSave"
|
||||||
<button type="button" class="secondary-btn" :disabled="saving" @click="handleDiscard">
|
@discard="handleDiscard"
|
||||||
DISCARD
|
/>
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<button type="button" class="primary-btn edit-btn" @click="handleEdit">EDIT</button>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Footer />
|
||||||
|
|
||||||
|
<EmailVerificationDialog
|
||||||
|
:visible="isVerificationDialogVisible"
|
||||||
|
:email="verificationTargetEmail"
|
||||||
|
:saving="saving"
|
||||||
|
@close="closeVerificationDialog"
|
||||||
|
@resend="handleSendVerifyCode"
|
||||||
|
@submit="handleVerificationSubmit"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from 'vue'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { ElMessage } from 'element-plus'
|
import EmailVerificationDialog from './components/EmailVerificationDialog.vue'
|
||||||
import Radio from './components/Radio.vue'
|
import ProfileSection from './components/ProfileSection.vue'
|
||||||
|
import RegionSection from './components/RegionSection.vue'
|
||||||
|
import SecuritySection from './components/SecuritySection.vue'
|
||||||
|
import SettingsActions from './components/SettingsActions.vue'
|
||||||
|
import { useSettingsForm } from './useSettingsForm'
|
||||||
|
|
||||||
interface SettingsData {
|
const { t, locale } = useI18n({ useScope: 'global' })
|
||||||
firstName: string
|
|
||||||
lastName: string
|
|
||||||
email: string
|
|
||||||
username: string
|
|
||||||
role: string[]
|
|
||||||
links: string[]
|
|
||||||
language: string
|
|
||||||
region: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SecurityDraft {
|
const {
|
||||||
newEmail: string
|
draftData,
|
||||||
newPassword: string
|
securityDraft,
|
||||||
currentPassword: string
|
isEditing,
|
||||||
}
|
saving,
|
||||||
|
isVerificationDialogVisible,
|
||||||
const roleList = [
|
verificationTargetEmail,
|
||||||
'Fashion Enthusiast',
|
roleList,
|
||||||
'Content Creator',
|
languageList,
|
||||||
'Student',
|
regionList,
|
||||||
'Retail / Buyer',
|
displayData,
|
||||||
'Fashion Designer',
|
isEmailVerified,
|
||||||
'Brand / Business',
|
displayLanguageLabel,
|
||||||
'PR & Communications',
|
displayRegionLabel,
|
||||||
'Stylist',
|
fullName,
|
||||||
'Graphic Designer',
|
roleModel,
|
||||||
'3D Artist',
|
handleEdit,
|
||||||
'Other'
|
handleDiscard,
|
||||||
].map((item) => ({ name: item, value: item }))
|
handleSave,
|
||||||
|
resetSecurityEmail,
|
||||||
const languageList = ['English', 'Chinese', 'Japanese']
|
resetSecurityPassword,
|
||||||
const regionList = ['Hong Kong SAR', 'Mainland China', 'Singapore', 'United Kingdom']
|
handleVerifyEmail,
|
||||||
|
handleSendVerifyCode,
|
||||||
const createDefaultData = (): SettingsData => ({
|
handleVerificationSubmit,
|
||||||
firstName: 'Alexandra',
|
closeVerificationDialog
|
||||||
lastName: 'Chen',
|
} = useSettingsForm({ t, locale })
|
||||||
email: 'alex.chen@gmail.com',
|
|
||||||
username: '@alexandra_chen',
|
|
||||||
role: ['Student', 'Graphic Designer'],
|
|
||||||
links: ['https://instagram.com/username', 'https://...'],
|
|
||||||
language: 'English',
|
|
||||||
region: 'Hong Kong SAR'
|
|
||||||
})
|
|
||||||
|
|
||||||
const cloneSettingsData = (data: SettingsData): SettingsData => ({
|
|
||||||
firstName: data.firstName,
|
|
||||||
lastName: data.lastName,
|
|
||||||
email: data.email,
|
|
||||||
username: data.username,
|
|
||||||
role: [...data.role],
|
|
||||||
links: [...data.links],
|
|
||||||
language: data.language,
|
|
||||||
region: data.region
|
|
||||||
})
|
|
||||||
|
|
||||||
const createEmptySecurityDraft = (): SecurityDraft => ({
|
|
||||||
newEmail: '',
|
|
||||||
newPassword: '',
|
|
||||||
currentPassword: ''
|
|
||||||
})
|
|
||||||
|
|
||||||
const sourceData = ref<SettingsData>(createDefaultData())
|
|
||||||
const draftData = ref<SettingsData>(cloneSettingsData(sourceData.value))
|
|
||||||
const securityDraft = ref<SecurityDraft>(createEmptySecurityDraft())
|
|
||||||
const isEditing = ref(false)
|
|
||||||
const saving = ref(false)
|
|
||||||
|
|
||||||
const displayData = computed(() => (isEditing.value ? draftData.value : sourceData.value))
|
|
||||||
|
|
||||||
const fullName = computed(() => {
|
|
||||||
const data = displayData.value
|
|
||||||
return `${data.firstName} ${data.lastName}`.trim()
|
|
||||||
})
|
|
||||||
|
|
||||||
const roleModel = computed({
|
|
||||||
get: () => displayData.value.role,
|
|
||||||
set: (value: string[]) => {
|
|
||||||
if (isEditing.value) {
|
|
||||||
draftData.value.role = value
|
|
||||||
return
|
|
||||||
}
|
|
||||||
sourceData.value.role = value
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const resetDraftState = () => {
|
|
||||||
draftData.value = cloneSettingsData(sourceData.value)
|
|
||||||
securityDraft.value = createEmptySecurityDraft()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleEdit = () => {
|
|
||||||
resetDraftState()
|
|
||||||
isEditing.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleAddLink = () => {
|
|
||||||
draftData.value.links.push('')
|
|
||||||
}
|
|
||||||
|
|
||||||
const resetSecurityEmail = () => {
|
|
||||||
securityDraft.value.newEmail = ''
|
|
||||||
}
|
|
||||||
|
|
||||||
const resetSecurityPassword = () => {
|
|
||||||
securityDraft.value.newPassword = ''
|
|
||||||
securityDraft.value.currentPassword = ''
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleDiscard = () => {
|
|
||||||
resetDraftState()
|
|
||||||
isEditing.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const buildNextData = () => {
|
|
||||||
const nextEmail = securityDraft.value.newEmail.trim() || draftData.value.email
|
|
||||||
return {
|
|
||||||
firstName: draftData.value.firstName.trim(),
|
|
||||||
lastName: draftData.value.lastName.trim(),
|
|
||||||
username: draftData.value.username.trim(),
|
|
||||||
email: nextEmail,
|
|
||||||
role: draftData.value.role,
|
|
||||||
links: draftData.value.links.map((item) => item.trim()).filter(Boolean),
|
|
||||||
language: draftData.value.language,
|
|
||||||
region: draftData.value.region
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSave = async () => {
|
|
||||||
const nextData = buildNextData()
|
|
||||||
saving.value = true
|
|
||||||
try {
|
|
||||||
sourceData.value = cloneSettingsData({
|
|
||||||
...nextData,
|
|
||||||
links: nextData.links.length ? nextData.links : ['']
|
|
||||||
})
|
|
||||||
draftData.value = cloneSettingsData(sourceData.value)
|
|
||||||
securityDraft.value = createEmptySecurityDraft()
|
|
||||||
isEditing.value = false
|
|
||||||
ElMessage.success('Settings updated')
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(error)
|
|
||||||
} finally {
|
|
||||||
saving.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
@@ -379,416 +111,45 @@ const handleSave = async () => {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
|
}
|
||||||
.field-text() {
|
|
||||||
font-family: 'KaiseiOpti-Regular';
|
.banner {
|
||||||
font-size: 1.6rem;
|
display: flex;
|
||||||
line-height: 2.4rem;
|
flex-direction: column;
|
||||||
color: #232323;
|
align-items: center;
|
||||||
}
|
justify-content: center;
|
||||||
|
height: 14.8rem;
|
||||||
.field-frame() {
|
row-gap: 1.2rem;
|
||||||
width: 100%;
|
background: linear-gradient(rgba(255, 255, 255, 0.91), rgba(255, 255, 255, 0.91)),
|
||||||
min-height: 4rem;
|
linear-gradient(90deg, #f2eee8 0%, #fbfaf8 40%, #f1ede7 100%);
|
||||||
border: 0.1rem solid #979797;
|
}
|
||||||
}
|
|
||||||
|
.title {
|
||||||
.control-wrapper() {
|
font-family: 'KaiseiOpti-Bold';
|
||||||
box-shadow: none;
|
font-size: 4rem;
|
||||||
border-radius: 0;
|
line-height: 3.6rem;
|
||||||
padding: 0 2rem;
|
color: #232323;
|
||||||
}
|
}
|
||||||
|
|
||||||
.banner {
|
.slogan {
|
||||||
height: 14.8rem;
|
font-family: 'KaiseiOpti-Regular';
|
||||||
row-gap: 1.2rem;
|
font-size: 1.6rem;
|
||||||
background: linear-gradient(rgba(255, 255, 255, 0.91), rgba(255, 255, 255, 0.91)),
|
line-height: 2.4rem;
|
||||||
linear-gradient(90deg, #f2eee8 0%, #fbfaf8 40%, #f1ede7 100%);
|
color: #585858;
|
||||||
|
}
|
||||||
.title {
|
|
||||||
font-family: 'KaiseiOpti-Bold';
|
.setting-content {
|
||||||
font-size: 4rem;
|
padding: 4rem 18rem 7rem;
|
||||||
line-height: 3.6rem;
|
}
|
||||||
color: #232323;
|
|
||||||
}
|
.gap {
|
||||||
|
height: 0.05rem;
|
||||||
.slogan {
|
margin-top: 6rem;
|
||||||
font-family: 'KaiseiOpti-Regular';
|
margin-bottom: 4rem;
|
||||||
font-size: 1.6rem;
|
background-color: #c4c4c4;
|
||||||
line-height: 2.4rem;
|
|
||||||
color: #585858;
|
&.bottom-gap {
|
||||||
}
|
margin-top: 4rem;
|
||||||
}
|
|
||||||
|
|
||||||
.setting-content {
|
|
||||||
padding: 4rem 18rem 7rem;
|
|
||||||
.setting-item {
|
|
||||||
column-gap: 21.4rem;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.label-container {
|
|
||||||
width: 27.6rem;
|
|
||||||
font-family: 'KaiseiOpti-Medium';
|
|
||||||
|
|
||||||
.label {
|
|
||||||
font-size: 2.8rem;
|
|
||||||
line-height: 3.6rem;
|
|
||||||
color: #232323;
|
|
||||||
}
|
|
||||||
|
|
||||||
.label-desc {
|
|
||||||
width: 24rem;
|
|
||||||
margin-top: 2rem;
|
|
||||||
font-size: 1.6rem;
|
|
||||||
line-height: 2.2rem;
|
|
||||||
color: #666666;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.context-container {
|
|
||||||
width: 58rem;
|
|
||||||
padding-top: 0.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-item-label,
|
|
||||||
.read-label,
|
|
||||||
.security-label {
|
|
||||||
margin-bottom: 0.8rem;
|
|
||||||
font-family: 'KaiseiOpti-Medium';
|
|
||||||
font-size: 1.4rem;
|
|
||||||
line-height: 2.4rem;
|
|
||||||
letter-spacing: 0.04em;
|
|
||||||
color: #585858;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-tip,
|
|
||||||
.read-tip,
|
|
||||||
.security-tip {
|
|
||||||
margin-top: 0.8rem;
|
|
||||||
font-family: 'KaiseiOpti-Regular';
|
|
||||||
font-size: 1.2rem;
|
|
||||||
line-height: 1.6rem;
|
|
||||||
color: #9f9f9f;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-item-value,
|
|
||||||
.outlined-field,
|
|
||||||
.link-href {
|
|
||||||
.field-frame();
|
|
||||||
|
|
||||||
:deep(.el-input),
|
|
||||||
:deep(.el-select) {
|
|
||||||
width: 100%;
|
|
||||||
min-height: 4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-input__wrapper),
|
|
||||||
:deep(.el-select__wrapper) {
|
|
||||||
.control-wrapper();
|
|
||||||
min-height: 4rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-item-value {
|
|
||||||
&.noborder {
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.name {
|
|
||||||
width: 28.4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.radio {
|
|
||||||
width: 58rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-input__inner) {
|
|
||||||
.field-text();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.read-box,
|
|
||||||
.field-box,
|
|
||||||
.security-static {
|
|
||||||
.field-frame();
|
|
||||||
.field-text();
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0.8rem 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.profile-header {
|
|
||||||
column-gap: 2.6rem;
|
|
||||||
margin-bottom: 3.6rem;
|
|
||||||
|
|
||||||
.avatar {
|
|
||||||
width: 8rem;
|
|
||||||
height: 8rem;
|
|
||||||
border-radius: 50%;
|
|
||||||
border: 0.1rem solid #d8d0c7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar-edit-icon {
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
width: 3rem;
|
|
||||||
height: 3rem;
|
|
||||||
border: 0.1rem solid #fff;
|
|
||||||
border-radius: 50%;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.profile-summary {
|
|
||||||
row-gap: 0.6rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.profile-name {
|
|
||||||
font-family: 'KaiseiOpti-Medium';
|
|
||||||
font-size: 2.4rem;
|
|
||||||
line-height: 3.6rem;
|
|
||||||
color: #232323;
|
|
||||||
}
|
|
||||||
|
|
||||||
.profile-email {
|
|
||||||
font-family: 'KaiseiOpti-Regular';
|
|
||||||
font-size: 1.8rem;
|
|
||||||
line-height: 2.4rem;
|
|
||||||
color: #979797;
|
|
||||||
}
|
|
||||||
|
|
||||||
.read-section {
|
|
||||||
width: 58rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-container {
|
|
||||||
row-gap: 3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.col-gap-2 {
|
|
||||||
column-gap: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.read-row + .read-row,
|
|
||||||
.region-row + .region-row {
|
|
||||||
margin-top: 3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.read-row.two-column {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 28.4rem 28.4rem;
|
|
||||||
column-gap: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.readonly-radio-group {
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.role-row {
|
|
||||||
margin-top: 3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.social-links {
|
|
||||||
margin-top: 5.8rem;
|
|
||||||
font-family: 'KaiseiOpti-Medium';
|
|
||||||
|
|
||||||
.title {
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
font-size: 1.4rem;
|
|
||||||
color: #585858;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.read-social-links {
|
|
||||||
margin-top: 4.8rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.links-list {
|
|
||||||
row-gap: 0.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.links-item {
|
|
||||||
column-gap: 3.4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.link-index {
|
|
||||||
font-family: 'KaiseiOpti-Regular';
|
|
||||||
font-size: 1.4rem;
|
|
||||||
line-height: 2rem;
|
|
||||||
color: #979797;
|
|
||||||
}
|
|
||||||
|
|
||||||
.link-href {
|
|
||||||
color: #979797;
|
|
||||||
|
|
||||||
&.readonly {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0.8rem 2rem;
|
|
||||||
font-family: 'KaiseiOpti-Regular';
|
|
||||||
font-size: 1.4rem;
|
|
||||||
line-height: 2rem;
|
|
||||||
color: #9f9f9f;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-input) {
|
|
||||||
height: 4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-input__inner) {
|
|
||||||
font-family: 'KaiseiOpti-Regular';
|
|
||||||
font-size: 1.4rem;
|
|
||||||
color: #9f9f9f;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.add-link-btn {
|
|
||||||
width: 4rem;
|
|
||||||
height: 4rem;
|
|
||||||
border: 0.1rem solid #f0ebe5;
|
|
||||||
background: #f6f6f6;
|
|
||||||
color: #b4aea6;
|
|
||||||
font-size: 2rem;
|
|
||||||
line-height: 1;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.security-container {
|
|
||||||
.security-row + .security-row {
|
|
||||||
margin-top: 2.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.security-label {
|
|
||||||
margin: 0 0 0.8rem;
|
|
||||||
|
|
||||||
&.inline {
|
|
||||||
width: 10.8rem;
|
|
||||||
margin-bottom: 0;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.security-static {
|
|
||||||
min-height: 2.4rem;
|
|
||||||
padding: 0.1rem 0 0;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.security-inline-row {
|
|
||||||
gap: 2.8rem;
|
|
||||||
min-height: 3.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.security-tip {
|
|
||||||
margin-top: 0.6rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.verify-field {
|
|
||||||
display: flex;
|
|
||||||
margin-top: 0.8rem;
|
|
||||||
|
|
||||||
:deep(.el-input) {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.verify-btn {
|
|
||||||
border: none;
|
|
||||||
height: 2.8rem;
|
|
||||||
line-height: 2.8rem;
|
|
||||||
border-left: 0.1rem solid #979797;
|
|
||||||
background: #ffffff;
|
|
||||||
font-family: 'KaiseiOpti-Medium';
|
|
||||||
font-size: 1.4rem;
|
|
||||||
color: #232323;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 0 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.password-mask {
|
|
||||||
font-family: 'KaiseiOpti-Bold';
|
|
||||||
letter-spacing: 0.08rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inner-divider {
|
|
||||||
height: 1px;
|
|
||||||
margin: 2rem 0;
|
|
||||||
background-color: #c4c4c4;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.region-container {
|
|
||||||
.field-box {
|
|
||||||
padding: 0.8rem 2rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.small-btn,
|
|
||||||
.secondary-btn,
|
|
||||||
.primary-btn {
|
|
||||||
border: 0.1rem solid #c4c4c4;
|
|
||||||
background: #f6f6f6;
|
|
||||||
font-family: 'KaiseiOpti-Bold';
|
|
||||||
color: #232323;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.small-btn {
|
|
||||||
width: 10rem;
|
|
||||||
height: 3.2rem;
|
|
||||||
align-self: flex-start;
|
|
||||||
font-size: 1.2rem;
|
|
||||||
line-height: 2.6rem;
|
|
||||||
letter-spacing: -0.03em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gap {
|
|
||||||
height: 0.05rem;
|
|
||||||
margin-top: 6rem;
|
|
||||||
margin-bottom: 4rem;
|
|
||||||
background-color: #c4c4c4;
|
|
||||||
|
|
||||||
&.bottom-gap {
|
|
||||||
margin-top: 4rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-container {
|
|
||||||
justify-content: center;
|
|
||||||
column-gap: 1.2rem;
|
|
||||||
margin-top: 2.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.primary-btn,
|
|
||||||
.secondary-btn {
|
|
||||||
height: 4.4rem;
|
|
||||||
font-size: 1.6rem;
|
|
||||||
line-height: 2.6rem;
|
|
||||||
|
|
||||||
&:disabled {
|
|
||||||
opacity: 0.6;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.primary-btn {
|
|
||||||
min-width: 23.6rem;
|
|
||||||
padding: 0 2.4rem;
|
|
||||||
border-color: #232323;
|
|
||||||
background: #232323;
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.secondary-btn {
|
|
||||||
width: 12rem;
|
|
||||||
background: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.edit-btn {
|
|
||||||
min-width: 12rem;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
46
src/views/setting/types.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
export const roleValues = [
|
||||||
|
'fashionEnthusiast',
|
||||||
|
'contentCreator',
|
||||||
|
'student',
|
||||||
|
'retailBuyer',
|
||||||
|
'fashionDesigner',
|
||||||
|
'brandBusiness',
|
||||||
|
'prCommunications',
|
||||||
|
'stylist',
|
||||||
|
'graphicDesigner',
|
||||||
|
'artist3d',
|
||||||
|
'other'
|
||||||
|
] as const
|
||||||
|
|
||||||
|
export const languageValues = ['english', 'chinese'] as const
|
||||||
|
export const regionValues = ['hongKongSar', 'mainlandChina', 'singapore', 'unitedKingdom'] as const
|
||||||
|
|
||||||
|
export type RoleValue = (typeof roleValues)[number]
|
||||||
|
export type LanguageValue = (typeof languageValues)[number]
|
||||||
|
export type RegionValue = (typeof regionValues)[number]
|
||||||
|
|
||||||
|
export interface SettingsData {
|
||||||
|
firstName: string
|
||||||
|
lastName: string
|
||||||
|
email: string
|
||||||
|
username: string
|
||||||
|
role: RoleValue[]
|
||||||
|
language: LanguageValue
|
||||||
|
region: RegionValue
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SecurityDraft {
|
||||||
|
newEmail: string
|
||||||
|
newPassword: string
|
||||||
|
currentPassword: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RoleOption {
|
||||||
|
name: string
|
||||||
|
value: RoleValue
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SettingOption<T extends string> {
|
||||||
|
label: string
|
||||||
|
value: T
|
||||||
|
}
|
||||||
288
src/views/setting/useSettingsForm.ts
Normal file
@@ -0,0 +1,288 @@
|
|||||||
|
import { computed, ref, shallowRef, watch, type Ref } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import {
|
||||||
|
languageValues,
|
||||||
|
regionValues,
|
||||||
|
roleValues,
|
||||||
|
type LanguageValue,
|
||||||
|
type RegionValue,
|
||||||
|
type RoleValue,
|
||||||
|
type SecurityDraft,
|
||||||
|
type SettingsData
|
||||||
|
} from './types'
|
||||||
|
|
||||||
|
type Translate = (key: string, ...args: unknown[]) => string
|
||||||
|
|
||||||
|
interface UseSettingsFormOptions {
|
||||||
|
t: Translate
|
||||||
|
locale: Ref<string>
|
||||||
|
}
|
||||||
|
|
||||||
|
const languageLocaleMap: Record<LanguageValue, 'ENGLISH' | 'CHINESE_SIMPLIFIED'> = {
|
||||||
|
english: 'ENGLISH',
|
||||||
|
chinese: 'CHINESE_SIMPLIFIED'
|
||||||
|
}
|
||||||
|
|
||||||
|
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||||||
|
|
||||||
|
const createDefaultData = (): SettingsData => ({
|
||||||
|
firstName: 'Alexandra',
|
||||||
|
lastName: 'Chen',
|
||||||
|
email: 'alex.chen@gmail.com',
|
||||||
|
username: '@alexandra_chen',
|
||||||
|
role: ['student', 'graphicDesigner'],
|
||||||
|
language: 'english',
|
||||||
|
region: 'hongKongSar'
|
||||||
|
})
|
||||||
|
|
||||||
|
const cloneSettingsData = (data: SettingsData): SettingsData => ({
|
||||||
|
firstName: data.firstName,
|
||||||
|
lastName: data.lastName,
|
||||||
|
email: data.email,
|
||||||
|
username: data.username,
|
||||||
|
role: [...data.role],
|
||||||
|
language: data.language,
|
||||||
|
region: data.region
|
||||||
|
})
|
||||||
|
|
||||||
|
const createEmptySecurityDraft = (): SecurityDraft => ({
|
||||||
|
newEmail: '',
|
||||||
|
newPassword: '',
|
||||||
|
currentPassword: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
export function useSettingsForm({ t, locale }: UseSettingsFormOptions) {
|
||||||
|
const sourceData = ref<SettingsData>(createDefaultData())
|
||||||
|
const draftData = ref<SettingsData>(cloneSettingsData(sourceData.value))
|
||||||
|
const securityDraft = ref<SecurityDraft>(createEmptySecurityDraft())
|
||||||
|
const isEditing = shallowRef(false)
|
||||||
|
const saving = shallowRef(false)
|
||||||
|
const isVerificationDialogVisible = shallowRef(false)
|
||||||
|
const verificationTargetEmail = shallowRef('')
|
||||||
|
const verifiedEmail = shallowRef('')
|
||||||
|
|
||||||
|
const roleList = computed(() =>
|
||||||
|
roleValues.map((value) => ({
|
||||||
|
name: t(`Settings.roles.${value}`),
|
||||||
|
value
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
|
const languageList = computed(() =>
|
||||||
|
languageValues.map((value) => ({
|
||||||
|
label: t(`Settings.languages.${value}`),
|
||||||
|
value
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
|
const regionList = computed(() =>
|
||||||
|
regionValues.map((value) => ({
|
||||||
|
label: t(`Settings.regions.${value}`),
|
||||||
|
value
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
|
const displayData = computed(() => (isEditing.value ? draftData.value : sourceData.value))
|
||||||
|
const normalizedNewEmail = computed(() => securityDraft.value.newEmail.trim())
|
||||||
|
const hasNewEmailChange = computed(
|
||||||
|
() => normalizedNewEmail.value.length > 0 && normalizedNewEmail.value !== sourceData.value.email
|
||||||
|
)
|
||||||
|
const isEmailVerified = computed(
|
||||||
|
() => hasNewEmailChange.value && verifiedEmail.value === normalizedNewEmail.value
|
||||||
|
)
|
||||||
|
const displayLanguageLabel = computed(() => t(`Settings.languages.${displayData.value.language}`))
|
||||||
|
const displayRegionLabel = computed(() => t(`Settings.regions.${displayData.value.region}`))
|
||||||
|
|
||||||
|
const fullName = computed(() => {
|
||||||
|
const data = displayData.value
|
||||||
|
return `${data.firstName} ${data.lastName}`.trim()
|
||||||
|
})
|
||||||
|
|
||||||
|
const roleModel = computed<RoleValue[]>({
|
||||||
|
get: () => displayData.value.role,
|
||||||
|
set: (value) => {
|
||||||
|
if (isEditing.value) {
|
||||||
|
draftData.value.role = value
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceData.value.role = value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const resetEmailVerificationState = () => {
|
||||||
|
isVerificationDialogVisible.value = false
|
||||||
|
verificationTargetEmail.value = ''
|
||||||
|
verifiedEmail.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const syncAppLanguage = (language: LanguageValue) => {
|
||||||
|
const nextLocale = languageLocaleMap[language]
|
||||||
|
locale.value = nextLocale
|
||||||
|
localStorage.setItem('language', nextLocale)
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetDraftState = () => {
|
||||||
|
draftData.value = cloneSettingsData(sourceData.value)
|
||||||
|
securityDraft.value = createEmptySecurityDraft()
|
||||||
|
resetEmailVerificationState()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEdit = () => {
|
||||||
|
resetDraftState()
|
||||||
|
isEditing.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetSecurityEmail = () => {
|
||||||
|
securityDraft.value.newEmail = ''
|
||||||
|
resetEmailVerificationState()
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetSecurityPassword = () => {
|
||||||
|
securityDraft.value.newPassword = ''
|
||||||
|
securityDraft.value.currentPassword = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDiscard = () => {
|
||||||
|
resetDraftState()
|
||||||
|
isEditing.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeVerificationDialog = () => {
|
||||||
|
isVerificationDialogVisible.value = false
|
||||||
|
verificationTargetEmail.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleVerifyEmail = () => {
|
||||||
|
const nextEmail = normalizedNewEmail.value
|
||||||
|
|
||||||
|
if (!nextEmail) {
|
||||||
|
ElMessage.warning(t('Settings.messages.enterNewEmailFirst'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!emailPattern.test(nextEmail)) {
|
||||||
|
ElMessage.warning(t('Settings.messages.invalidEmail'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextEmail === sourceData.value.email) {
|
||||||
|
ElMessage.warning(t('Settings.messages.sameEmail'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (verifiedEmail.value === nextEmail) {
|
||||||
|
ElMessage.success(t('Settings.messages.alreadyVerified'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
verificationTargetEmail.value = nextEmail
|
||||||
|
handleSendVerifyCode()
|
||||||
|
isVerificationDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSendVerifyCode = () => {
|
||||||
|
ElMessage.success(t('Settings.messages.verificationCodeSent'))
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleVerificationSubmit = (code: string) => {
|
||||||
|
if (code.length !== 6) {
|
||||||
|
ElMessage.warning(t('Settings.messages.enterVerificationCode'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
verifiedEmail.value = verificationTargetEmail.value
|
||||||
|
closeVerificationDialog()
|
||||||
|
ElMessage.success(t('Settings.messages.verificationCompleted'))
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildNextData = (): SettingsData => {
|
||||||
|
const nextEmail = securityDraft.value.newEmail.trim() || draftData.value.email
|
||||||
|
|
||||||
|
return {
|
||||||
|
firstName: draftData.value.firstName.trim(),
|
||||||
|
lastName: draftData.value.lastName.trim(),
|
||||||
|
username: draftData.value.username.trim(),
|
||||||
|
email: nextEmail,
|
||||||
|
role: [...draftData.value.role],
|
||||||
|
language: draftData.value.language,
|
||||||
|
region: draftData.value.region
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
if (hasNewEmailChange.value && !isEmailVerified.value) {
|
||||||
|
ElMessage.warning(t('Settings.messages.verifyEmailBeforeSave'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextData = buildNextData()
|
||||||
|
const previousLanguage = sourceData.value.language
|
||||||
|
saving.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
sourceData.value = cloneSettingsData(nextData)
|
||||||
|
|
||||||
|
if (nextData.language !== previousLanguage) {
|
||||||
|
syncAppLanguage(nextData.language)
|
||||||
|
}
|
||||||
|
|
||||||
|
draftData.value = cloneSettingsData(sourceData.value)
|
||||||
|
securityDraft.value = createEmptySecurityDraft()
|
||||||
|
resetEmailVerificationState()
|
||||||
|
isEditing.value = false
|
||||||
|
ElMessage.success(t('Settings.messages.settingsUpdated'))
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(error)
|
||||||
|
} finally {
|
||||||
|
saving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => securityDraft.value.newEmail,
|
||||||
|
(value) => {
|
||||||
|
const trimmedValue = value.trim()
|
||||||
|
|
||||||
|
if (verifiedEmail.value && trimmedValue !== verifiedEmail.value) {
|
||||||
|
verifiedEmail.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
isVerificationDialogVisible.value &&
|
||||||
|
verificationTargetEmail.value &&
|
||||||
|
trimmedValue !== verificationTargetEmail.value
|
||||||
|
) {
|
||||||
|
closeVerificationDialog()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
sourceData,
|
||||||
|
draftData,
|
||||||
|
securityDraft,
|
||||||
|
isEditing,
|
||||||
|
saving,
|
||||||
|
isVerificationDialogVisible,
|
||||||
|
verificationTargetEmail,
|
||||||
|
roleList,
|
||||||
|
languageList,
|
||||||
|
regionList,
|
||||||
|
displayData,
|
||||||
|
isEmailVerified,
|
||||||
|
displayLanguageLabel,
|
||||||
|
displayRegionLabel,
|
||||||
|
fullName,
|
||||||
|
roleModel,
|
||||||
|
handleEdit,
|
||||||
|
handleDiscard,
|
||||||
|
handleSave,
|
||||||
|
resetSecurityEmail,
|
||||||
|
resetSecurityPassword,
|
||||||
|
handleVerifyEmail,
|
||||||
|
handleSendVerifyCode,
|
||||||
|
handleVerificationSubmit,
|
||||||
|
closeVerificationDialog
|
||||||
|
}
|
||||||
|
}
|
||||||
216
src/views/shopping-drawer.vue
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, onUnmounted, reactive, toRefs } from "vue";
|
||||||
|
import myEvent from '@/utils/myEvent'
|
||||||
|
// import scList from '@/views/shoppingCart/sc-list.vue'
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
import img from '@/assets/images/brand-null.png'
|
||||||
|
|
||||||
|
//const props = defineProps({
|
||||||
|
//})
|
||||||
|
//const emit = defineEmits([
|
||||||
|
//])
|
||||||
|
let data = reactive({
|
||||||
|
})
|
||||||
|
const router = useRouter()
|
||||||
|
const isShoppingShow = ref(true)
|
||||||
|
const shoppingClose = () => {
|
||||||
|
isShoppingShow.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const goShopping = () => {
|
||||||
|
router.push({path: '/shoppingCart'})
|
||||||
|
isShoppingShow.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(()=>{
|
||||||
|
myEvent.add('addShopping', (item) => {
|
||||||
|
isShoppingShow.value = true
|
||||||
|
console.log(item)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
onUnmounted(()=>{
|
||||||
|
myEvent.remove('addShopping')
|
||||||
|
})
|
||||||
|
defineExpose({})
|
||||||
|
const {} = toRefs(data);
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<el-drawer v-model="isShoppingShow" width="50rem" class="addShoppingDrawer" :close-on-click-modal="false" title="I am the title" :with-header="false">
|
||||||
|
<div class="addShoppingInfo">
|
||||||
|
<div class="header">
|
||||||
|
<div class="title">Added to your Shopping Cart</div>
|
||||||
|
<span class="close" @click="shoppingClose"
|
||||||
|
><svg-icon name="close" size="13"
|
||||||
|
/></span>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<div class="img-list">
|
||||||
|
<div class="img-box">
|
||||||
|
<img :src="img" alt="">
|
||||||
|
</div>
|
||||||
|
<div class="img-box">
|
||||||
|
<img :src="img" alt="">
|
||||||
|
</div>
|
||||||
|
<div class="img-box">
|
||||||
|
<img :src="img" alt="">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="inf-box">
|
||||||
|
<div class="name">North Outfit Set</div>
|
||||||
|
<div class="shopping-name">
|
||||||
|
<div class="icon">
|
||||||
|
<SvgIcon name="shop" size="24" />
|
||||||
|
</div>
|
||||||
|
Roaming Clouds
|
||||||
|
</div>
|
||||||
|
<div class="price">$15 <span class="currency">HKD</span></div>
|
||||||
|
</div>
|
||||||
|
<div class="statement">
|
||||||
|
<div class="icon">
|
||||||
|
<SvgIcon name="statement" size="16.6" />
|
||||||
|
</div>
|
||||||
|
Digital Assets Only. No physical product included.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="button" @click="goShopping">
|
||||||
|
SeE Shopping Cart
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- <sc-list is-mini style="flex: 0.6;" @close="shoppingClose"/> -->
|
||||||
|
</el-drawer>
|
||||||
|
</template>
|
||||||
|
<style lang="less">
|
||||||
|
.el-drawer.addShoppingDrawer{
|
||||||
|
--el-drawer-padding-primary: 2.4rem 3.4rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
|
||||||
|
.addShoppingInfo{
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
> .header{
|
||||||
|
border-bottom: 0.1rem solid #c4c4c4;
|
||||||
|
display: flex;
|
||||||
|
padding-bottom: 2.4rem;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
> .title{
|
||||||
|
font-family: KaiseiOpti-Bold;
|
||||||
|
font-size: 2rem;
|
||||||
|
line-height: 120%;
|
||||||
|
color: #121212;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
> .close{
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
> .content{
|
||||||
|
flex: 1;
|
||||||
|
padding-top: 9.6rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
> .img-list{
|
||||||
|
height: 37.5rem;
|
||||||
|
width: 33.7rem;
|
||||||
|
position: relative;
|
||||||
|
> .img-box{
|
||||||
|
position: absolute;
|
||||||
|
overflow: hidden;
|
||||||
|
width: 26.9rem;
|
||||||
|
height: 34.1rem;
|
||||||
|
border: 1px solid #EFEFEF;
|
||||||
|
top: 1.7rem;
|
||||||
|
right: 0;
|
||||||
|
left: auto;
|
||||||
|
transform-origin: bottom;
|
||||||
|
box-shadow: 1rem .8rem 2.4rem 0px #4D4D4D0A;
|
||||||
|
> img{
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
&:nth-child(1){
|
||||||
|
transform: rotate(-8deg);
|
||||||
|
background-color: #eaeaea;
|
||||||
|
right: 2rem;
|
||||||
|
}
|
||||||
|
&:nth-child(2){
|
||||||
|
transform: rotate(-4deg);
|
||||||
|
background-color: #eeeeee;
|
||||||
|
right: 1rem;
|
||||||
|
}
|
||||||
|
&:nth-child(3){
|
||||||
|
transform: rotate(0);
|
||||||
|
background-color: #fafafa;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
> .inf-box{
|
||||||
|
margin-top: 5.18rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
> .name{
|
||||||
|
font-family: KaiseiOpti-Bold;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 2.4rem;
|
||||||
|
line-height: 140%;
|
||||||
|
}
|
||||||
|
> .shopping-name{
|
||||||
|
margin-top: 1.3rem;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 1.6rem;
|
||||||
|
line-height: 140%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
> .icon{
|
||||||
|
margin-right: 0.8rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
> .price{
|
||||||
|
margin-top: 1.2rem;
|
||||||
|
font-family: KaiseiOpti-Bold;
|
||||||
|
font-weight: 700;
|
||||||
|
font-style: Bold;
|
||||||
|
font-size: 1.8rem;
|
||||||
|
line-height: 140%;
|
||||||
|
> .currency{
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 140%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
> .statement{
|
||||||
|
margin-top: 5rem;
|
||||||
|
font-family: KaiseiOpti-Regular;
|
||||||
|
color: #979797;
|
||||||
|
display: flex;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
line-height: 140%;
|
||||||
|
> .icon{
|
||||||
|
margin-right: 0.8rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
> .button{
|
||||||
|
font-family: KaiseiOpti-Regular;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 4.6rem;
|
||||||
|
letter-spacing: 3%;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid #232323;
|
||||||
|
margin-bottom: calc(6rem - 2.4rem);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
48
src/views/shoppingCart/index.vue
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<template>
|
||||||
|
<div class="shopping-cart">
|
||||||
|
<div class="content">
|
||||||
|
<sc-list @selected-change="(v) => (selectedList = v)" />
|
||||||
|
<order-summary :list="selectedList" />
|
||||||
|
<!-- <sc-list is-mini style="height: 70rem" /> -->
|
||||||
|
<!-- <sc-list is-mini is-view title="Order Summary" style="height: 70rem" /> -->
|
||||||
|
</div>
|
||||||
|
<my-footer />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, ref } from 'vue'
|
||||||
|
import orderSummary from './order-summary.vue'
|
||||||
|
import scList from './sc-list.vue'
|
||||||
|
import myFooter from '@/components/Footer.vue'
|
||||||
|
const selectedList = ref([])
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.shopping-cart {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
--content-top: 4.8rem;
|
||||||
|
> .content {
|
||||||
|
max-width: 126rem;
|
||||||
|
padding-top: var(--content-top);
|
||||||
|
margin: 0 auto;
|
||||||
|
min-height: var(--app-view-height);
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
> .sc-list {
|
||||||
|
flex: 1;
|
||||||
|
margin-right: 7.5rem;
|
||||||
|
margin-bottom: 8rem;
|
||||||
|
--sc-list-header-top: var(--content-top);
|
||||||
|
}
|
||||||
|
> .order-summary {
|
||||||
|
position: sticky;
|
||||||
|
top: var(--content-top);
|
||||||
|
max-height: var(--app-view-height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
166
src/views/shoppingCart/order-summary.vue
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
<template>
|
||||||
|
<div class="order-summary">
|
||||||
|
<div class="title">Order Summary</div>
|
||||||
|
<div class="count">
|
||||||
|
<span class="label">Selected</span>
|
||||||
|
<span class="value">{{ brandsList.length }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="hr"></div>
|
||||||
|
<div class="brands-header">
|
||||||
|
<span class="icon"><svg-icon name="order-shop" size="24" /></span>
|
||||||
|
<span class="text">Brands</span>
|
||||||
|
</div>
|
||||||
|
<div class="brands-item" v-for="v in brandsList" :key="v.brand">
|
||||||
|
<span class="label">{{ v.brand }}</span>
|
||||||
|
<span class="value"
|
||||||
|
><span>{{ v.children.length }}</span
|
||||||
|
>item</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<div class="total">
|
||||||
|
<span class="label">Total</span>
|
||||||
|
<span class="value"
|
||||||
|
><span>${{ totalAmount }}</span> HKD</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="hr"></div>
|
||||||
|
<button class="checkout-btn" custom="black" @click="handleCheckout">CHECKOUT SELECTED</button>
|
||||||
|
<div class="tip">Digital assets. Creator retains copyright.</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
import { FormatBytes, FormatDate } from '@/utils/tools'
|
||||||
|
const props = defineProps({
|
||||||
|
list: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const brandsList = computed(() => {
|
||||||
|
const arr = []
|
||||||
|
props.list.forEach((v) => {
|
||||||
|
const index = arr.findIndex((v_) => v_.brand === v.brand)
|
||||||
|
if (index === -1) {
|
||||||
|
arr.push({
|
||||||
|
brand: v.brand,
|
||||||
|
children: [v]
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
arr[index].children.push(v)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return arr
|
||||||
|
})
|
||||||
|
const totalAmount = computed(() => props.list.reduce((pre, cur) => pre + cur.amount, 0).toFixed(2))
|
||||||
|
const handleCheckout = () => {
|
||||||
|
console.log('购买:', props.list)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.order-summary {
|
||||||
|
width: 39.8rem;
|
||||||
|
padding: 3rem 2rem 3rem 2.4rem;
|
||||||
|
height: auto;
|
||||||
|
background-color: #f6f6f6;
|
||||||
|
> .title {
|
||||||
|
font-family: KaiseiOpti-Bold;
|
||||||
|
font-size: 2.4rem;
|
||||||
|
line-height: 2.3rem;
|
||||||
|
color: #232323;
|
||||||
|
margin-bottom: 1.8rem;
|
||||||
|
}
|
||||||
|
> div {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
> .count {
|
||||||
|
color: #232323;
|
||||||
|
> .label {
|
||||||
|
font-size: 1.6rem;
|
||||||
|
}
|
||||||
|
> .value {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
> .hr {
|
||||||
|
margin: 1.2rem 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 0;
|
||||||
|
border-top: 0.1rem solid #c4c4c4;
|
||||||
|
}
|
||||||
|
> .brands-header {
|
||||||
|
justify-content: flex-start;
|
||||||
|
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
> .icon {
|
||||||
|
width: 2.4rem;
|
||||||
|
height: 2.4rem;
|
||||||
|
margin-right: 0.4rem;
|
||||||
|
}
|
||||||
|
> .text {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
color: #232323;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
> .brands-item {
|
||||||
|
margin-bottom: 0.8rem;
|
||||||
|
padding-left: 1rem;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
> .label {
|
||||||
|
text-decoration: underline;
|
||||||
|
color: #585858;
|
||||||
|
}
|
||||||
|
> .value {
|
||||||
|
color: #808080;
|
||||||
|
&:deep(span) {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
color: #585858;
|
||||||
|
margin-right: 0.8rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
> .total-file-size {
|
||||||
|
> .label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
> .icon {
|
||||||
|
width: 2.4rem;
|
||||||
|
height: 2.4rem;
|
||||||
|
margin-right: 0.4rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
> .value {
|
||||||
|
> span {
|
||||||
|
color: #808080;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
> .total {
|
||||||
|
> .value {
|
||||||
|
color: #585858;
|
||||||
|
> span {
|
||||||
|
font-size: 2.2rem;
|
||||||
|
color: #232323;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
> .checkout-btn {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 3rem;
|
||||||
|
}
|
||||||
|
> .tip {
|
||||||
|
margin-top: 1rem;
|
||||||
|
font-family: KaiseiOpti-Regular;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
justify-content: center;
|
||||||
|
color: #808080;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
200
src/views/shoppingCart/sc-item.vue
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
<template>
|
||||||
|
<div class="sc-item" :class="{ 'is-order-actions-layout': orderActionsLayout }">
|
||||||
|
<slot name="checkbox" />
|
||||||
|
<img :src="info.url" />
|
||||||
|
<div class="content">
|
||||||
|
<div class="title">{{ info.title }}</div>
|
||||||
|
<div class="brand">
|
||||||
|
<span class="icon"><svg-icon name="order-shop" size="24" /></span>
|
||||||
|
<span class="text">{{ info.brand }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="tags" v-if="showTags">
|
||||||
|
<span v-for="tag in info.tags" :key="tag" class="tag">{{ tag }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="date" v-if="showDate">
|
||||||
|
<!-- <div class="icon"><svg-icon name="order-file" size="18" /></div> -->
|
||||||
|
<div class="text">
|
||||||
|
{{ FormatDate(info.date, 'SM D, YYYY, h:mm A') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="right">
|
||||||
|
<div class="amount">${{ info.amount }}<span> HKD</span></div>
|
||||||
|
<SvgIcon
|
||||||
|
v-if="orderActionsLayout"
|
||||||
|
class="download"
|
||||||
|
name="download"
|
||||||
|
size="32"
|
||||||
|
color="#232323"
|
||||||
|
/>
|
||||||
|
<div class="remove" v-if="showRemove" @click="onRemove">
|
||||||
|
<span class="icon"><svg-icon name="order-delete" size="18" /></span>
|
||||||
|
<span class="text">Remove</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref, onMounted } from 'vue'
|
||||||
|
import { FormatBytes, FormatDate } from '@/utils/tools'
|
||||||
|
const emit = defineEmits(['remove'])
|
||||||
|
const props = defineProps({
|
||||||
|
showTags: { type: Boolean, default: true },
|
||||||
|
showDate: { type: Boolean, default: true },
|
||||||
|
showRemove: { type: Boolean, default: true },
|
||||||
|
orderActionsLayout: { type: Boolean, default: false },
|
||||||
|
info: { type: Object, default: () => {} }
|
||||||
|
})
|
||||||
|
const onRemove = () => {
|
||||||
|
emit('remove', props.info.id)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.sc-item {
|
||||||
|
border-bottom: 0.1rem solid #c4c4c4;
|
||||||
|
padding: var(--sc-item-padding, 2.4rem 0);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
> img {
|
||||||
|
width: var(--sc-item-img-width, 14.8rem);
|
||||||
|
height: var(--sc-item-img-height, 18.8rem);
|
||||||
|
object-fit: contain;
|
||||||
|
background-color: #f6f6f6;
|
||||||
|
}
|
||||||
|
> .content {
|
||||||
|
flex: 1;
|
||||||
|
margin: var(--sc-item-content-margin, 0 4rem);
|
||||||
|
align-self: var(--sc-item-content-align-self);
|
||||||
|
> * {
|
||||||
|
margin-bottom: var(--sc-item-margin-bottom, 1.6rem);
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
> .title {
|
||||||
|
font-family: KaiseiOpti-Bold;
|
||||||
|
font-size: var(--sc-item-title-font-size, 2.4rem);
|
||||||
|
color: #232323;
|
||||||
|
}
|
||||||
|
> .brand {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
> .icon {
|
||||||
|
width: 2.4rem;
|
||||||
|
height: 2.4rem;
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
> .text {
|
||||||
|
font-size: var(--sc-item-brand-font-size, 1.6rem);
|
||||||
|
text-decoration: underline;
|
||||||
|
color: #232323;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
> .tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.8rem;
|
||||||
|
> .tag {
|
||||||
|
min-width: var(--sc-item-tag-min-width, 8.8rem);
|
||||||
|
height: var(--sc-item-tag-height, 2.4rem);
|
||||||
|
line-height: var(--sc-item-tag-height, 2.4rem);
|
||||||
|
border-radius: var(--sc-item-tag-radius, 2.4rem);
|
||||||
|
font-size: var(--sc-item-tag-font-size, 1.4rem);
|
||||||
|
padding: var(--sc-item-tag-padding, 0 1rem);
|
||||||
|
text-align: center;
|
||||||
|
color: #8f8f8f;
|
||||||
|
background-color: #eee;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
> .date {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
> .icon {
|
||||||
|
width: 2.4rem;
|
||||||
|
height: 2.4rem;
|
||||||
|
margin-right: 1rem;
|
||||||
|
color: #808080;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .text {
|
||||||
|
font-family: KaiseiOpti-Regular;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
color: #808080;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
> .right {
|
||||||
|
align-self: var(--sc-item-right-align-self, end);
|
||||||
|
display: var(--sc-item-right-display);
|
||||||
|
flex-direction: var(--sc-item-right-flex-direction);
|
||||||
|
justify-content: var(--sc-item-right-justify-content);
|
||||||
|
align-items: var(--sc-item-right-align-items);
|
||||||
|
height: var(--sc-item-right-height);
|
||||||
|
margin-top: var(--sc-item-right-margin-top);
|
||||||
|
> .amount {
|
||||||
|
font-family: KaiseiOpti-Bold;
|
||||||
|
font-size: var(--sc-item-amount-font-size, 2.2rem);
|
||||||
|
color: #232323;
|
||||||
|
> span {
|
||||||
|
font-size: var(--sc-item-currency-font-size, 1.4rem);
|
||||||
|
color: #585858;
|
||||||
|
vertical-align: bottom;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
> .remove {
|
||||||
|
margin-top: var(--sc-item-remove-margin-top, 9rem);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
user-select: none;
|
||||||
|
cursor: pointer;
|
||||||
|
> .icon {
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
margin-right: 0.4rem;
|
||||||
|
}
|
||||||
|
> .text {
|
||||||
|
font-family: KaiseiOpti-Regular;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
color: #808080;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.is-order-actions-layout {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns:
|
||||||
|
var(--sc-item-img-width, 14.8rem)
|
||||||
|
minmax(0, 1fr)
|
||||||
|
var(--sc-item-order-amount-width, 12rem)
|
||||||
|
var(--sc-item-order-action-width, 18rem);
|
||||||
|
column-gap: var(--sc-item-order-column-gap, 2rem);
|
||||||
|
|
||||||
|
> .content {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .right {
|
||||||
|
display: contents;
|
||||||
|
|
||||||
|
> .amount {
|
||||||
|
grid-column: 3;
|
||||||
|
align-self: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
transform: translateX(var(--sc-item-order-actions-offset, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
.c-svg {
|
||||||
|
width: initial;
|
||||||
|
height: initial;
|
||||||
|
}
|
||||||
|
> .download {
|
||||||
|
grid-column: 4;
|
||||||
|
cursor: pointer;
|
||||||
|
transform: translateX(var(--sc-item-order-actions-offset, 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
64
src/views/shoppingCart/sc-list-null.vue
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
<template>
|
||||||
|
<div class="sc-list-null">
|
||||||
|
<img v-if="nullImage === 'shopping-cart'" src="@/assets/images/shopping-cart-null.png" />
|
||||||
|
<img v-else-if="nullImage === 'brand'" src="@/assets/images/brand-null.png" />
|
||||||
|
<div class="title">{{ title }}</div>
|
||||||
|
<div class="tip">{{ tip }}</div>
|
||||||
|
<button custom v-show="showButton" @click="handleClick">{{ buttonText }}</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref, onMounted } from 'vue'
|
||||||
|
const props = defineProps({
|
||||||
|
nullImage: { type: String as () => 'shopping-cart' | 'brand', default: 'shopping-cart' },
|
||||||
|
title: { type: String, default: '' },
|
||||||
|
tip: { type: String, default: '' },
|
||||||
|
showButton: { type: Boolean, default: true },
|
||||||
|
buttonText: { type: String, default: 'EXPLORE DIGITAL ITEMS' }
|
||||||
|
})
|
||||||
|
const emit = defineEmits(['explore'])
|
||||||
|
const handleClick = () => {
|
||||||
|
console.log('emit("explore")')
|
||||||
|
emit('explore')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.sc-list-null {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: center;
|
||||||
|
> img {
|
||||||
|
width: 14rem;
|
||||||
|
height: auto;
|
||||||
|
margin-bottom: 2.4rem;
|
||||||
|
}
|
||||||
|
> .title {
|
||||||
|
font-family: KaiseiOpti-Bold;
|
||||||
|
font-size: 1.6rem;
|
||||||
|
line-height: 2.4rem;
|
||||||
|
color: #979797;
|
||||||
|
margin-bottom: 0.8rem;
|
||||||
|
}
|
||||||
|
> .tip {
|
||||||
|
width: 50%;
|
||||||
|
font-family: KaiseiOpti-Regular;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
line-height: 2.4rem;
|
||||||
|
color: #979797;
|
||||||
|
}
|
||||||
|
> button {
|
||||||
|
min-width: 30rem;
|
||||||
|
height: 4.4rem;
|
||||||
|
border: 0.1rem solid #c4c4c4;
|
||||||
|
font-family: KaiseiOpti-Medium;
|
||||||
|
font-size: 1.6rem;
|
||||||
|
color: #979797;
|
||||||
|
margin-top: 3rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
436
src/views/shoppingCart/sc-list.vue
Normal file
@@ -0,0 +1,436 @@
|
|||||||
|
<template>
|
||||||
|
<!-- 购物车列表 -->
|
||||||
|
<div class="sc-list" :class="{ mini: isMini, view: isView }">
|
||||||
|
<div class="header">
|
||||||
|
<div class="title">
|
||||||
|
<span class="text">{{ title || 'Shopping Cart' }}</span>
|
||||||
|
<span class="close" v-if="isMini && !isView" @click="onClose"
|
||||||
|
><svg-icon name="close" size="13"
|
||||||
|
/></span>
|
||||||
|
</div>
|
||||||
|
<div class="options" v-if="!isMini">
|
||||||
|
<div class="left">
|
||||||
|
<el-checkbox
|
||||||
|
:model-value="allSelected"
|
||||||
|
:indeterminate="selectedCount === 0 ? false : selectedCount < list.length"
|
||||||
|
@click="handleAllAllClick"
|
||||||
|
/>
|
||||||
|
<span class="count">{{ selectedCount }} Selected</span>
|
||||||
|
<div class="hr"></div>
|
||||||
|
<div class="btn" @click="handleAllAllClick(true)">Select All</div>
|
||||||
|
<div class="btn" @click="handleAllAllClick(false)">Deselect All</div>
|
||||||
|
</div>
|
||||||
|
<div class="right">
|
||||||
|
<el-select v-model="sortBy" placeholder="Sort By" :teleported="false">
|
||||||
|
<template #label="{ label }">
|
||||||
|
<span class="header-label">Sort By</span>
|
||||||
|
<span class="header-value">{{ label }}</span>
|
||||||
|
</template>
|
||||||
|
<el-option
|
||||||
|
v-for="item in sortByOptions"
|
||||||
|
:key="item.label"
|
||||||
|
:value="item.value"
|
||||||
|
:label="item.label"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="list">
|
||||||
|
<sc-list-null
|
||||||
|
v-show="list.length === 0"
|
||||||
|
:show-button="!isMini"
|
||||||
|
title="Your Cart is empty"
|
||||||
|
tip="Discover new fashion assets and add them to your cart."
|
||||||
|
/>
|
||||||
|
<sc-item
|
||||||
|
v-for="v in list"
|
||||||
|
:key="v.id"
|
||||||
|
:info="v"
|
||||||
|
:show-tags="!isMini || isView"
|
||||||
|
:show-date="!isMini"
|
||||||
|
:show-remove="!isView"
|
||||||
|
@remove="handleRemoveClick"
|
||||||
|
>
|
||||||
|
<template #checkbox>
|
||||||
|
<el-checkbox v-model="v.checked" v-if="!isMini" @change="handleSelectedChange" />
|
||||||
|
</template>
|
||||||
|
</sc-item>
|
||||||
|
</div>
|
||||||
|
<div class="footer" v-if="isMini">
|
||||||
|
<div class="total" v-show="list.length > 0 || isView">
|
||||||
|
<span class="label">Total</span>
|
||||||
|
<span class="value">${{ allAmount }}<span> HKD</span></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tip" v-if="isView">Digital assets. Creator retains copyright.</div>
|
||||||
|
<button custom="black" v-if="!isView">CHECKOUT</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref, onMounted } from 'vue'
|
||||||
|
import { FormatBytes, FormatDate } from '@/utils/tools'
|
||||||
|
import scItem from './sc-item.vue'
|
||||||
|
import scListNull from './sc-list-null.vue'
|
||||||
|
const emit = defineEmits(['close', 'selected-change'])
|
||||||
|
const props = defineProps({
|
||||||
|
title: { type: String, default: '' },
|
||||||
|
isMini: { type: Boolean, default: false },
|
||||||
|
isView: { type: Boolean, default: false }
|
||||||
|
})
|
||||||
|
const allTotalSize = computed(() => {
|
||||||
|
const total = list.value.reduce((pre, cur) => pre + cur.fileSize, 0)
|
||||||
|
const str = FormatBytes(total, { unitBig: true })
|
||||||
|
return {
|
||||||
|
size: str.split(' ')[0],
|
||||||
|
unit: str.split(' ')[1]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const allAmount = computed(() => list.value.reduce((pre, cur) => pre + cur.amount, 0).toFixed(2))
|
||||||
|
const selectedCount = computed(() => list.value.filter((v) => v.checked).length)
|
||||||
|
const allSelected = computed(() =>
|
||||||
|
list.value.length === 0 ? false : list.value.every((v) => v.checked)
|
||||||
|
)
|
||||||
|
const sortBy = ref('')
|
||||||
|
const sortByOptions = ref([
|
||||||
|
{
|
||||||
|
label: 'Default',
|
||||||
|
value: 'Default'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Selected First',
|
||||||
|
value: 'Selected First'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Date Added',
|
||||||
|
value: 'Date Added'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
const list = ref([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
url: 'http://118.31.39.42:3000/falls/shopping-cart-1.png',
|
||||||
|
title: 'North Outfit Set',
|
||||||
|
brand: 'Roaming Clouds',
|
||||||
|
date: '2026-5-20 5:20',
|
||||||
|
amount: 49.99,
|
||||||
|
tags: ['female', 'skirt', 'blouse', 'outwear'],
|
||||||
|
checked: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
url: 'http://118.31.39.42:3000/falls/shopping-cart-2.png',
|
||||||
|
title: 'Weekend Drift Co-ord',
|
||||||
|
brand: 'Urban Line Edit',
|
||||||
|
date: '2026-5-21 13:14',
|
||||||
|
amount: 9.99,
|
||||||
|
tags: ['female', 'skirt', 'blouse', 'outwear'],
|
||||||
|
checked: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
url: 'http://118.31.39.42:3000/falls/shopping-cart-3.png',
|
||||||
|
title: 'Static Street Suit',
|
||||||
|
brand: 'Off Grid Apparel',
|
||||||
|
date: '2026-5-21 13:14',
|
||||||
|
amount: 12,
|
||||||
|
tags: ['female', 'skirt', 'blouse', 'outwear'],
|
||||||
|
checked: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
url: 'http://118.31.39.42:3000/falls/shopping-cart-4.png',
|
||||||
|
title: 'Maison Contour Suit',
|
||||||
|
brand: 'Ivory Muse Studio',
|
||||||
|
date: '2026-5-21 13:14',
|
||||||
|
amount: 18,
|
||||||
|
tags: ['female', 'skirt', 'blouse', 'outwear'],
|
||||||
|
checked: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
url: 'http://118.31.39.42:3000/falls/shopping-cart-5.png',
|
||||||
|
title: 'Prime Atelier Set',
|
||||||
|
brand: 'Ivory Muse Studio',
|
||||||
|
date: '2026-5-21 13:14',
|
||||||
|
amount: 20,
|
||||||
|
tags: ['female', 'skirt', 'blouse', 'outwear'],
|
||||||
|
checked: false
|
||||||
|
}
|
||||||
|
])
|
||||||
|
const handleAllAllClick = (checked?: boolean) => {
|
||||||
|
const checked_ = typeof checked === 'boolean' ? checked : !allSelected.value
|
||||||
|
list.value.forEach((v) => (v.checked = checked_))
|
||||||
|
handleSelectedChange()
|
||||||
|
}
|
||||||
|
const handleSelectedChange = () => {
|
||||||
|
const arr = list.value.filter((v) => v.checked)
|
||||||
|
emit('selected-change', arr)
|
||||||
|
}
|
||||||
|
const onClose = () => {
|
||||||
|
emit('close')
|
||||||
|
}
|
||||||
|
onMounted(() => {
|
||||||
|
handleSelectedChange()
|
||||||
|
})
|
||||||
|
const handleRemoveClick = (id: number) => {
|
||||||
|
list.value = list.value.filter((v) => v.id !== id)
|
||||||
|
handleSelectedChange()
|
||||||
|
}
|
||||||
|
const handleExploreClick = () => {
|
||||||
|
console.log('探索')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.sc-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
&.mini {
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
--sc-list-title-font-size: 2rem;
|
||||||
|
--sc-list-title-padding-bottom: 3rem;
|
||||||
|
--sc-item-padding: 2rem 0;
|
||||||
|
--sc-item-img-width: 10.4rem;
|
||||||
|
--sc-item-img-height: 13.2rem;
|
||||||
|
--sc-item-content-margin: 0 2rem;
|
||||||
|
--sc-item-margin-bottom: 0.8rem;
|
||||||
|
--sc-item-title-font-size: 1.6rem;
|
||||||
|
--sc-item-brand-font-size: 1.4rem;
|
||||||
|
--sc-item-amount-font-size: 1.8rem;
|
||||||
|
--sc-item-currency-font-size: 1.6rem;
|
||||||
|
--sc-item-currency-font-size: 1.6rem;
|
||||||
|
--sc-item-content-align-self: baseline;
|
||||||
|
--sc-item-right-display: flex;
|
||||||
|
--sc-item-right-flex-direction: column;
|
||||||
|
--sc-item-right-justify-content: space-between;
|
||||||
|
--sc-item-right-align-items: flex-end;
|
||||||
|
--sc-item-right-height: var(--sc-item-img-height);
|
||||||
|
> .list {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 30rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.mini.view {
|
||||||
|
--sc-list-title-padding-bottom: 2.4rem;
|
||||||
|
--sc-item-img-width: 9.6rem;
|
||||||
|
--sc-item-img-height: 12.2rem;
|
||||||
|
--sc-item-padding: 1.6rem;
|
||||||
|
--sc-item-margin-bottom: 0.8rem;
|
||||||
|
--sc-item-title-font-size: 2rem;
|
||||||
|
--sc-item-right-margin-top: 1.2rem;
|
||||||
|
--sc-item-content-margin: 1.2rem 2rem 0;
|
||||||
|
--sc-item-tag-min-width: 0;
|
||||||
|
--sc-item-tag-height: 2rem;
|
||||||
|
--sc-item-tag-radius: 2rem;
|
||||||
|
--sc-item-tag-font-size: 1.2rem;
|
||||||
|
--sc-item-tag-padding: 0 0.8rem;
|
||||||
|
> .header {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
> .footer {
|
||||||
|
margin-top: 4.1rem;
|
||||||
|
> .total {
|
||||||
|
margin-bottom: 1.7rem;
|
||||||
|
> .label {
|
||||||
|
font-family: KaiseiOpti-Medium;
|
||||||
|
font-size: 2rem;
|
||||||
|
color: #232323;
|
||||||
|
}
|
||||||
|
> .value {
|
||||||
|
font-family: KaiseiOpti-Medium;
|
||||||
|
font-size: 2.2rem;
|
||||||
|
> span {
|
||||||
|
font-family: KaiseiOpti-Regular;
|
||||||
|
font-size: 1.8rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
> .tip {
|
||||||
|
font-family: KaiseiOpti-Regular;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
color: #808080;
|
||||||
|
margin-top: 2.4rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&:not(.mini) {
|
||||||
|
> .header {
|
||||||
|
position: sticky;
|
||||||
|
top: var(--sc-list-header-top, 0);
|
||||||
|
background-color: #fff;
|
||||||
|
z-index: 999;
|
||||||
|
&:after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 1px;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: calc(var(--sc-list-header-top, 0) + 2px);
|
||||||
|
transform: translateY(-100%);
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
> .list {
|
||||||
|
> .sc-list-null {
|
||||||
|
margin-top: 10rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
> .header {
|
||||||
|
border-bottom: 0.1rem solid #c4c4c4;
|
||||||
|
padding-bottom: var(--sc-list-title-padding-bottom, 3rem);
|
||||||
|
> .title {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
> .text {
|
||||||
|
font-family: KaiseiOpti-Bold;
|
||||||
|
font-size: var(--sc-list-title-font-size, 4rem);
|
||||||
|
color: #121212;
|
||||||
|
}
|
||||||
|
> .close {
|
||||||
|
width: 2.4rem;
|
||||||
|
height: 2.4rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
> .options {
|
||||||
|
margin-top: 1rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
> .left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
> .count {
|
||||||
|
font-family: KaiseiOpti-Regular;
|
||||||
|
font-size: 1.8rem;
|
||||||
|
color: #232323;
|
||||||
|
}
|
||||||
|
> .hr {
|
||||||
|
width: 0;
|
||||||
|
height: 100%;
|
||||||
|
border-left: 0.1rem solid #c4c4c4;
|
||||||
|
margin: 0 2rem;
|
||||||
|
}
|
||||||
|
> .btn {
|
||||||
|
margin-right: 1.2rem;
|
||||||
|
font-family: KaiseiOpti-Regular;
|
||||||
|
font-size: 1.8rem;
|
||||||
|
color: #979797;
|
||||||
|
text-decoration: underline;
|
||||||
|
user-select: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
> .list {
|
||||||
|
}
|
||||||
|
> .footer {
|
||||||
|
margin-top: 3rem;
|
||||||
|
> .total {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
> .label {
|
||||||
|
font-family: KaiseiOpti-Bold;
|
||||||
|
font-size: 1.6rem;
|
||||||
|
color: #121212;
|
||||||
|
}
|
||||||
|
> .value {
|
||||||
|
font-family: KaiseiOpti-Bold;
|
||||||
|
font-size: 2rem;
|
||||||
|
color: #232323;
|
||||||
|
> span {
|
||||||
|
font-family: KaiseiOpti-Medium;
|
||||||
|
font-size: 1.6rem;
|
||||||
|
color: #585858;
|
||||||
|
vertical-align: bottom;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
> button {
|
||||||
|
width: 100%;
|
||||||
|
height: 4.6rem;
|
||||||
|
--button-font-size: 1.4rem;
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.sc-list:deep(.el-checkbox) {
|
||||||
|
margin-right: 3rem;
|
||||||
|
height: auto;
|
||||||
|
--el-checkbox-font-size: 1.4rem;
|
||||||
|
--el-checkbox-input-width: 2.4rem;
|
||||||
|
--el-checkbox-input-height: 2.4rem;
|
||||||
|
--el-checkbox-checked-bg-color: #000;
|
||||||
|
--el-checkbox-checked-input-border-color: #000;
|
||||||
|
--el-checkbox-input-border: 0.1rem solid #232323;
|
||||||
|
--el-checkbox-bg-color: #fff;
|
||||||
|
--el-checkbox-border-radius: 0;
|
||||||
|
.el-checkbox__input.is-indeterminate .el-checkbox__inner:before {
|
||||||
|
height: 0.2rem;
|
||||||
|
width: 65%;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: scale(1) translate(-50%, -50%);
|
||||||
|
border-radius: 0.1rem;
|
||||||
|
}
|
||||||
|
.el-checkbox__inner:after {
|
||||||
|
width: 0.6rem;
|
||||||
|
height: 1.3rem;
|
||||||
|
border-width: 0.2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.sc-list:deep(.el-select) {
|
||||||
|
width: 15rem;
|
||||||
|
--el-border-radius-base: 0;
|
||||||
|
--el-select-input-color: rgba(0, 0, 0, 0.5);
|
||||||
|
--el-select-input-font-size: 1rem;
|
||||||
|
.el-select__wrapper {
|
||||||
|
font-size: 1.07rem;
|
||||||
|
padding: 0 0.7rem;
|
||||||
|
line-height: 1;
|
||||||
|
min-height: 0;
|
||||||
|
height: 2.2rem;
|
||||||
|
|
||||||
|
.header-label {
|
||||||
|
font-family: KaiseiOpti-Regular;
|
||||||
|
color: rgba(0, 0, 0, 0.5);
|
||||||
|
margin-right: 0.6rem;
|
||||||
|
}
|
||||||
|
.header-value {
|
||||||
|
font-family: KaiseiOpti-Bold;
|
||||||
|
color: #232323;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.el-select__popper {
|
||||||
|
--el-popper-border-radius: 0;
|
||||||
|
border: 0.1rem solid #d0d0d0;
|
||||||
|
.el-select-dropdown__list {
|
||||||
|
padding: 0;
|
||||||
|
> .el-select-dropdown__item {
|
||||||
|
margin-bottom: 0.89rem;
|
||||||
|
color: #232323;
|
||||||
|
font-size: 1.069rem;
|
||||||
|
height: 2.68rem;
|
||||||
|
line-height: 2.68rem;
|
||||||
|
padding: 0 1.4rem;
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
&.is-selected {
|
||||||
|
font-family: KaiseiOpti-Bold;
|
||||||
|
background-color: #f4f4f4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
550
src/views/wardrobe/Assets.vue
Normal file
@@ -0,0 +1,550 @@
|
|||||||
|
<template>
|
||||||
|
<div class="wardrobe-assets flex">
|
||||||
|
<aside class="wardrobe-assets__filters">
|
||||||
|
<div class="filters-card">
|
||||||
|
<div class="filters-card__heading">
|
||||||
|
<h2 class="filters-card__title">Filters</h2>
|
||||||
|
<button class="filters-card__clear" type="button" @click="clearFilters">Clear</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="filter-group">
|
||||||
|
<h3 class="filter-group__title">Categories</h3>
|
||||||
|
<div class="filter-group__line"></div>
|
||||||
|
<div class="filter-group__options">
|
||||||
|
<button
|
||||||
|
v-for="option in categories"
|
||||||
|
:key="option.value"
|
||||||
|
class="filter-option"
|
||||||
|
type="button"
|
||||||
|
:class="{ 'is-active': isCategoryActive(option.value) }"
|
||||||
|
@click="toggleCategory(option.value)"
|
||||||
|
>
|
||||||
|
<span class="filter-option__box">
|
||||||
|
<span class="filter-option__tick"></span>
|
||||||
|
</span>
|
||||||
|
<span class="filter-option__label">{{ option.label }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="filter-group">
|
||||||
|
<h3 class="filter-group__title">Gender</h3>
|
||||||
|
<div class="filter-group__line"></div>
|
||||||
|
<div class="filter-group__options">
|
||||||
|
<button
|
||||||
|
v-for="option in genders"
|
||||||
|
:key="option.value"
|
||||||
|
class="filter-option"
|
||||||
|
type="button"
|
||||||
|
:class="{ 'is-active': filters.gender === option.value }"
|
||||||
|
@click="setGender(option.value)"
|
||||||
|
>
|
||||||
|
<span class="filter-option__box">
|
||||||
|
<span class="filter-option__tick"></span>
|
||||||
|
</span>
|
||||||
|
<span class="filter-option__label">{{ option.label }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<section
|
||||||
|
class="wardrobe-assets__content flex flex-1 flex-col"
|
||||||
|
:class="{ 'has-data': dataList.length }"
|
||||||
|
>
|
||||||
|
<div class="assets-toolbar">
|
||||||
|
<div class="assets-toolbar__selection">
|
||||||
|
<div class="select-count flex align-center">
|
||||||
|
<img src="@/assets/images/wardrobe/select.png" />
|
||||||
|
<span class="assets-toolbar__count">{{ selectedCount }} Selected</span>
|
||||||
|
</div>
|
||||||
|
<div class="assets-toolbar__link" @click="handleSelectAll(true)">Select All</div>
|
||||||
|
<div class="assets-toolbar__link" @click="handleSelectAll(false)">Deselect All</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="assets-toolbar__actions">
|
||||||
|
<div
|
||||||
|
class="assets-toolbar__download flex flex-center"
|
||||||
|
:class="{ disabled: selectedCount < 1 }"
|
||||||
|
@click="handleDownloadSelected"
|
||||||
|
>
|
||||||
|
<SvgIcon name="downloadBtn" color="#fff" />
|
||||||
|
<span>Download Selected</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="dataList.length" class="data-list-container">
|
||||||
|
<div ref="dataListRef" class="data-list">
|
||||||
|
<div
|
||||||
|
v-for="(item, index) in dataList"
|
||||||
|
:key="item.url"
|
||||||
|
class="item"
|
||||||
|
:class="{ 'is-last-column': isLastColumn(index) }"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
v-show="item.checked"
|
||||||
|
src="@/assets/images/wardrobe/checked.png"
|
||||||
|
@click="handleSelectItem(item)"
|
||||||
|
class="checkbox"
|
||||||
|
/>
|
||||||
|
<div v-show="!item.checked" class="checkbox" @click="handleSelectItem(item)" />
|
||||||
|
<CommodityItem
|
||||||
|
download
|
||||||
|
:url="item.url"
|
||||||
|
:name="item.title"
|
||||||
|
:price="item.price"
|
||||||
|
:showPrice="false"
|
||||||
|
></CommodityItem>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Empty v-else @explore="goToDigitalItems" />
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, nextTick, onMounted, onUnmounted, reactive, ref, shallowRef, watch } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import img from '@/assets/images/collectionStory/Rectangle.png'
|
||||||
|
import Empty from './Empty.vue'
|
||||||
|
|
||||||
|
interface FilterOption {
|
||||||
|
label: string
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const categories: FilterOption[] = [
|
||||||
|
{ label: 'All', value: 'all' },
|
||||||
|
{ label: 'Outerwear', value: 'outerwear' },
|
||||||
|
{ label: 'Dress', value: 'dress' },
|
||||||
|
{ label: 'Trousers', value: 'trousers' },
|
||||||
|
{ label: 'Blouse', value: 'blouse' },
|
||||||
|
{ label: 'Skirt', value: 'skirt' },
|
||||||
|
{ label: 'Accessories', value: 'accessories' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const genders: FilterOption[] = [
|
||||||
|
{ label: 'All', value: 'all' },
|
||||||
|
{ label: 'Male', value: 'male' },
|
||||||
|
{ label: 'Female', value: 'female' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const categoryValues = categories
|
||||||
|
.filter((option) => option.value !== 'all')
|
||||||
|
.map((option) => option.value)
|
||||||
|
|
||||||
|
const filters = reactive({
|
||||||
|
categories: ['skirt'] as string[],
|
||||||
|
gender: 'all'
|
||||||
|
})
|
||||||
|
|
||||||
|
const dataList = ref([
|
||||||
|
{
|
||||||
|
url: img,
|
||||||
|
title: 'Windswept Burden',
|
||||||
|
price: '$100.00',
|
||||||
|
checked: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: img,
|
||||||
|
title: 'Windswept Burden',
|
||||||
|
price: '$100.00',
|
||||||
|
checked: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: img,
|
||||||
|
title: 'Windswept Burden',
|
||||||
|
price: '$100.00',
|
||||||
|
checked: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: img,
|
||||||
|
title: 'Windswept Burden',
|
||||||
|
price: '$100.00',
|
||||||
|
checked: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: img,
|
||||||
|
title: 'Windswept Burden',
|
||||||
|
price: '$100.00',
|
||||||
|
checked: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: img,
|
||||||
|
title: 'Windswept Burden',
|
||||||
|
price: '$100.00',
|
||||||
|
checked: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: img,
|
||||||
|
title: 'Windswept Burden',
|
||||||
|
price: '$100.00',
|
||||||
|
checked: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: img,
|
||||||
|
title: 'Windswept Burden',
|
||||||
|
price: '$100.00',
|
||||||
|
checked: false
|
||||||
|
}
|
||||||
|
])
|
||||||
|
const dataListRef = ref<HTMLDivElement | null>(null)
|
||||||
|
const gridColumnCount = shallowRef(1)
|
||||||
|
let gridResizeObserver: ResizeObserver | null = null
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => filters,
|
||||||
|
(val) => {
|
||||||
|
console.log(val)
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
const selectedCount = computed(() => {
|
||||||
|
return dataList.value.filter((el) => el.checked === true).length
|
||||||
|
})
|
||||||
|
const allCategoriesSelected = computed(() => {
|
||||||
|
return (
|
||||||
|
filters.categories.length === categoryValues.length &&
|
||||||
|
categoryValues.every((value) => filters.categories.includes(value))
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const isCategoryActive = (value: string) => {
|
||||||
|
if (value === 'all') {
|
||||||
|
return allCategoriesSelected.value
|
||||||
|
}
|
||||||
|
|
||||||
|
return filters.categories.includes(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleCategory = (value: string) => {
|
||||||
|
if (value === 'all') {
|
||||||
|
filters.categories = allCategoriesSelected.value ? [] : [...categoryValues]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.categories.includes(value)) {
|
||||||
|
filters.categories = filters.categories.filter((item) => item !== value)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
filters.categories = [...filters.categories, value]
|
||||||
|
}
|
||||||
|
|
||||||
|
const setGender = (value: string) => {
|
||||||
|
filters.gender = value
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearFilters = () => {
|
||||||
|
filters.categories = [...categoryValues]
|
||||||
|
filters.gender = 'all'
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSelectItem = (item) => {
|
||||||
|
console.log('111', item)
|
||||||
|
item.checked = !item.checked
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSelectAll = (flag) => {
|
||||||
|
dataList.value.forEach((item) => {
|
||||||
|
item.checked = flag
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDownloadSelected = () => {
|
||||||
|
const items = dataList.value.filter((item) => item.checked)
|
||||||
|
console.log(items)
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateGridColumnCount = () => {
|
||||||
|
if (!dataListRef.value) {
|
||||||
|
gridColumnCount.value = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const templateColumns = window.getComputedStyle(dataListRef.value).gridTemplateColumns
|
||||||
|
const columnCount =
|
||||||
|
templateColumns && templateColumns !== 'none'
|
||||||
|
? templateColumns.split(' ').filter(Boolean).length
|
||||||
|
: 1
|
||||||
|
|
||||||
|
gridColumnCount.value = Math.max(columnCount, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const isLastColumn = (index: number) => {
|
||||||
|
return (index + 1) % gridColumnCount.value === 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToDigitalItems = () => {
|
||||||
|
router.push('/digitalItem')
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
nextTick(() => {
|
||||||
|
updateGridColumnCount()
|
||||||
|
|
||||||
|
if (!dataListRef.value || typeof ResizeObserver === 'undefined') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
gridResizeObserver = new ResizeObserver(() => {
|
||||||
|
updateGridColumnCount()
|
||||||
|
})
|
||||||
|
gridResizeObserver.observe(dataListRef.value)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
gridResizeObserver?.disconnect()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.c-svg {
|
||||||
|
width: initial;
|
||||||
|
height: initial;
|
||||||
|
}
|
||||||
|
.wardrobe-assets {
|
||||||
|
--wardrobe-border-color: #d9d4cd;
|
||||||
|
--wardrobe-border-dark: #c8c0b4;
|
||||||
|
--wardrobe-text-main: #232323;
|
||||||
|
--wardrobe-text-secondary: #7a746d;
|
||||||
|
--wardrobe-text-muted: #a0978b;
|
||||||
|
height: 100%;
|
||||||
|
// overflow: hidden;
|
||||||
|
padding: 0 9rem 0 10rem;
|
||||||
|
|
||||||
|
.wardrobe-assets__filters {
|
||||||
|
width: 26.4rem;
|
||||||
|
border-right: 0.1rem solid var(--wardrobe-border-color);
|
||||||
|
// background: #fffcf7;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
.filters-card {
|
||||||
|
padding: 3rem 2.4rem 4rem;
|
||||||
|
|
||||||
|
.filters-card__heading {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 3.2rem;
|
||||||
|
|
||||||
|
.filters-card__title {
|
||||||
|
margin: 0;
|
||||||
|
font-family: 'KaiseiOpti-Bold';
|
||||||
|
font-size: 2.4rem;
|
||||||
|
line-height: 1.2;
|
||||||
|
color: var(--wardrobe-text-main);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters-card__clear {
|
||||||
|
border: 0;
|
||||||
|
padding: 0;
|
||||||
|
background: transparent;
|
||||||
|
font-family: 'KaiseiOpti-Regular';
|
||||||
|
font-size: 1.4rem;
|
||||||
|
line-height: 1.3;
|
||||||
|
color: #9a9185;
|
||||||
|
text-decoration: underline;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-group {
|
||||||
|
& + .filter-group {
|
||||||
|
margin-top: 3.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-group__title {
|
||||||
|
margin: 0 0 1rem;
|
||||||
|
font-family: 'KaiseiOpti-Bold';
|
||||||
|
font-size: 1.8rem;
|
||||||
|
line-height: 1.3;
|
||||||
|
color: #5e5851;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-group__line {
|
||||||
|
width: 100%;
|
||||||
|
height: 0.1rem;
|
||||||
|
background: var(--wardrobe-border-color);
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-group__options {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.2rem;
|
||||||
|
|
||||||
|
.filter-option {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1.2rem;
|
||||||
|
width: fit-content;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
font-family: 'KaiseiOpti-Regular';
|
||||||
|
font-size: 1.5rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
color: #6e665d;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
|
> .filter-option__box {
|
||||||
|
width: 1.6rem;
|
||||||
|
height: 1.6rem;
|
||||||
|
border: 0.1rem solid var(--wardrobe-border-dark);
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: #ffffff;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
.filter-option__tick {
|
||||||
|
width: 0.9rem;
|
||||||
|
height: 0.5rem;
|
||||||
|
border-left: 0.18rem solid #ffffff;
|
||||||
|
border-bottom: 0.18rem solid #ffffff;
|
||||||
|
transform: rotate(-45deg) translateY(-0.05rem);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-active {
|
||||||
|
color: var(--wardrobe-text-main);
|
||||||
|
|
||||||
|
.filter-option__box {
|
||||||
|
border-color: var(--wardrobe-text-main);
|
||||||
|
background: var(--wardrobe-text-main);
|
||||||
|
|
||||||
|
.filter-option__tick {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.wardrobe-assets__content {
|
||||||
|
border-right: 0.05rem solid #585858;
|
||||||
|
// &.has-data {
|
||||||
|
// border: none;
|
||||||
|
// }
|
||||||
|
|
||||||
|
.assets-toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 1.8rem 1.2rem;
|
||||||
|
border-bottom: 0.05rem solid #585858;
|
||||||
|
|
||||||
|
.assets-toolbar__selection {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1.4rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assets-toolbar__selection {
|
||||||
|
font-family: 'KaiseiOpti-Regular';
|
||||||
|
.select-count {
|
||||||
|
column-gap: 1.2rem;
|
||||||
|
img{
|
||||||
|
width: 2.4rem;
|
||||||
|
height: 2.4rem ;
|
||||||
|
}
|
||||||
|
.assets-toolbar__count {
|
||||||
|
position: relative;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
line-height: 1.2;
|
||||||
|
color: #57524b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.assets-toolbar__link {
|
||||||
|
border: 0;
|
||||||
|
padding: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
color: #a0978b;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.assets-toolbar__actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.assets-toolbar__download {
|
||||||
|
height: 4.4rem;
|
||||||
|
padding: 0 3rem;
|
||||||
|
border: 0.1rem solid #232323;
|
||||||
|
background: #232323;
|
||||||
|
font-family: 'KaiseiOpti-Regular';
|
||||||
|
font-size: 1.3rem;
|
||||||
|
color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
column-gap: 1.2rem;
|
||||||
|
&.disabled {
|
||||||
|
background-color: #979797;
|
||||||
|
border-color: #979797;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-list-container {
|
||||||
|
overflow-y: auto;
|
||||||
|
padding-bottom: 8rem;
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.data-list {
|
||||||
|
width: 100%;
|
||||||
|
flex: 1;
|
||||||
|
display: grid;
|
||||||
|
align-content: start;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(min(100%, 28rem), 1fr));
|
||||||
|
border-top: 0.05rem solid #585858;
|
||||||
|
border-left: 0.05rem solid #585858;
|
||||||
|
|
||||||
|
.item {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 1.2rem;
|
||||||
|
background: #ffffff;
|
||||||
|
position: relative;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-bottom: 0.05rem solid #585858;
|
||||||
|
|
||||||
|
&:not(.is-last-column) {
|
||||||
|
border-right: 0.05rem solid #585858;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox {
|
||||||
|
position: absolute;
|
||||||
|
top: 1.2rem;
|
||||||
|
left: 1.2rem;
|
||||||
|
width: 2.4rem;
|
||||||
|
height: 2.4rem;
|
||||||
|
border: 0.15rem solid #585858;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.commodity-item) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
56
src/views/wardrobe/Empty.vue
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'explore'): void
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="wardrobe-empty flex flex-col flex-center">
|
||||||
|
<img src="@/assets/images/wardrobe/empty-wardrobe.png" class="wardrobe-empty__image" alt="" />
|
||||||
|
|
||||||
|
<h2 class="wardrobe-empty__title">Nothing in Wardrobe yet</h2>
|
||||||
|
<p class="wardrobe-empty__description">
|
||||||
|
Explore the digital item and add pieces to your collection.
|
||||||
|
</p>
|
||||||
|
<button class="wardrobe-empty__button" type="button" @click="emit('explore')">
|
||||||
|
Explore Digital Items
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.wardrobe-empty {
|
||||||
|
flex: 1;
|
||||||
|
color: #979797;
|
||||||
|
|
||||||
|
> .wardrobe-empty__image {
|
||||||
|
width: 14.2rem;
|
||||||
|
height: 18.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .wardrobe-empty__title {
|
||||||
|
font-family: 'KaiseiOpti-Bold';
|
||||||
|
font-size: 1.6rem;
|
||||||
|
margin: 2.4rem 0 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .wardrobe-empty__description {
|
||||||
|
font-family: 'KaiseiOpti-Regular';
|
||||||
|
font-size: 1.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .wardrobe-empty__button {
|
||||||
|
margin-top: 3rem;
|
||||||
|
height: 4.4rem;
|
||||||
|
line-height: 4.4rem;
|
||||||
|
padding: 0 3.8rem;
|
||||||
|
border: 0.1rem solid #c4c4c4;
|
||||||
|
background: #ffffff;
|
||||||
|
font-family: 'KaiseiOpti-Regular';
|
||||||
|
font-size: 1.6rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: #585858;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
425
src/views/wardrobe/Orders.vue
Normal file
@@ -0,0 +1,425 @@
|
|||||||
|
<template>
|
||||||
|
<div class="wardrobe-orders">
|
||||||
|
<div class="orders-toolbar">
|
||||||
|
<button
|
||||||
|
v-for="status in statusOptions"
|
||||||
|
:key="status.key"
|
||||||
|
class="orders-toolbar__chip"
|
||||||
|
type="button"
|
||||||
|
:class="{ 'is-active': activeStatus === status.key }"
|
||||||
|
@click="activeStatus = status.key"
|
||||||
|
>
|
||||||
|
{{ status.label }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="orders-list">
|
||||||
|
<article v-for="order in filteredOrders" :key="order.id" class="order-card">
|
||||||
|
<button class="order-card__toggle" type="button" @click="toggleOrder(order.id)">
|
||||||
|
<span :class="{ 'is-expanded': expandedOrderId === order.id }"></span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="order-card__summary">
|
||||||
|
<div class="order-card__meta">
|
||||||
|
<h3 class="order-card__id">{{ order.id }}</h3>
|
||||||
|
<p class="order-card__date">{{ order.date }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="order-card__preview" aria-hidden="true">
|
||||||
|
<span
|
||||||
|
v-for="item in order.items.slice(0, 2)"
|
||||||
|
:key="item.id"
|
||||||
|
class="order-card__thumb"
|
||||||
|
:style="{ backgroundColor: item.color }"
|
||||||
|
></span>
|
||||||
|
<span v-if="getExtraCount(order)" class="order-card__extra">
|
||||||
|
+{{ getExtraCount(order) }} more
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="order-card__status" :class="`is-${order.status}`">
|
||||||
|
{{ order.status.toUpperCase() }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="order-card__amount">
|
||||||
|
<span>${{ order.amount }}</span>
|
||||||
|
<small>HKD</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="order-card__action"
|
||||||
|
type="button"
|
||||||
|
:class="{ 'is-primary': order.status === 'unpaid' }"
|
||||||
|
>
|
||||||
|
<svg-icon v-if="order.status === 'paid'" name="Invoice" size="20" color="#232323" />
|
||||||
|
<span v-if="order.status === 'paid'">Invoice</span>
|
||||||
|
<span v-else-if="order.status === 'unpaid'">Complete Payment</span>
|
||||||
|
<span v-else>Buy Again</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="expandedOrderId === order.id" class="order-card__details">
|
||||||
|
<ScItem
|
||||||
|
v-for="item in order.items"
|
||||||
|
:key="item.id"
|
||||||
|
class="order-card__item"
|
||||||
|
:style="{ '--order-item-placeholder': item.color }"
|
||||||
|
:info="item"
|
||||||
|
:show-date="false"
|
||||||
|
:show-remove="false"
|
||||||
|
order-actions-layout
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, shallowRef } from 'vue'
|
||||||
|
import ScItem from '@/views/shoppingCart/sc-item.vue'
|
||||||
|
|
||||||
|
type OrderStatus = 'all' | 'paid' | 'unpaid' | 'cancelled'
|
||||||
|
type ActualOrderStatus = Exclude<OrderStatus, 'all'>
|
||||||
|
|
||||||
|
interface StatusOption {
|
||||||
|
key: OrderStatus
|
||||||
|
label: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OrderItem {
|
||||||
|
id: number
|
||||||
|
url: string
|
||||||
|
title: string
|
||||||
|
brand: string
|
||||||
|
fileSize: number
|
||||||
|
date: string
|
||||||
|
amount: number
|
||||||
|
tags: string[]
|
||||||
|
checked: boolean
|
||||||
|
color: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OrderRecord {
|
||||||
|
id: string
|
||||||
|
date: string
|
||||||
|
status: ActualOrderStatus
|
||||||
|
amount: number
|
||||||
|
items: OrderItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const placeholderImage = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw=='
|
||||||
|
|
||||||
|
const statusOptions: StatusOption[] = [
|
||||||
|
{ key: 'all', label: 'All' },
|
||||||
|
{ key: 'paid', label: 'Paid' },
|
||||||
|
{ key: 'unpaid', label: 'Unpaid' },
|
||||||
|
{ key: 'cancelled', label: 'Canceled' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const createOrderItem = (
|
||||||
|
id: number,
|
||||||
|
title: string,
|
||||||
|
brand: string,
|
||||||
|
amount: number,
|
||||||
|
color: string
|
||||||
|
): OrderItem => {
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
url: placeholderImage,
|
||||||
|
title,
|
||||||
|
brand,
|
||||||
|
fileSize: 1024 * (id + 2),
|
||||||
|
date: '2026-02-16 23:34',
|
||||||
|
amount,
|
||||||
|
color,
|
||||||
|
checked: false,
|
||||||
|
tags: ['female', 'dress', 'blouse', 'outwear']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const orders: OrderRecord[] = [
|
||||||
|
{
|
||||||
|
id: 'SP897772698',
|
||||||
|
date: 'Feb 23, 2026, 3:00 PM',
|
||||||
|
status: 'paid',
|
||||||
|
amount: 45,
|
||||||
|
items: [
|
||||||
|
createOrderItem(1, 'North Outfit Set', 'Roaming Clouds', 15, '#e7e1d7'),
|
||||||
|
createOrderItem(2, 'Velvet Night Dress', 'Ivory Muse Studio', 16, '#5d2f5e'),
|
||||||
|
createOrderItem(3, 'Maison Contour Suit', 'Ivory Muse Studio', 14, '#dcd8d1')
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'SP893872698',
|
||||||
|
date: 'Feb 21, 2026, 10:20 AM',
|
||||||
|
status: 'unpaid',
|
||||||
|
amount: 15,
|
||||||
|
items: [createOrderItem(4, 'Silver Drape Dress', 'Roaming Clouds', 15, '#d8d3ca')]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'SP897262698',
|
||||||
|
date: 'Feb 16, 2026, 11:34 PM',
|
||||||
|
status: 'paid',
|
||||||
|
amount: 29,
|
||||||
|
items: [
|
||||||
|
createOrderItem(5, 'North Outfit Set', 'Roaming Clouds', 15, '#ece8df'),
|
||||||
|
createOrderItem(6, 'North Outfit Set', 'Ivory Muse Studio', 5, '#d5ddd7'),
|
||||||
|
createOrderItem(7, 'North Outfit Set', 'Ivory Muse Studio', 9, '#e5e1d9')
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'SP892072692',
|
||||||
|
date: 'Feb 2, 2026, 9:34 PM',
|
||||||
|
status: 'paid',
|
||||||
|
amount: 15,
|
||||||
|
items: [
|
||||||
|
createOrderItem(8, 'Cream Jacket Set', 'Roaming Clouds', 7, '#eee3d3'),
|
||||||
|
createOrderItem(9, 'White Linen Dress', 'Ivory Muse Studio', 8, '#d8d8d8')
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'SP892972603',
|
||||||
|
date: 'Jan 4, 2026, 8:22 PM',
|
||||||
|
status: 'cancelled',
|
||||||
|
amount: 15,
|
||||||
|
items: [createOrderItem(10, 'Soft Utility Knit', 'Urban Line Edit', 15, '#d8c2a4')]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const activeStatus = shallowRef<OrderStatus>('all')
|
||||||
|
const expandedOrderId = shallowRef('SP897262698')
|
||||||
|
|
||||||
|
const filteredOrders = computed(() => {
|
||||||
|
if (activeStatus.value === 'all') return orders
|
||||||
|
return orders.filter((order) => order.status === activeStatus.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
const toggleOrder = (orderId: string) => {
|
||||||
|
expandedOrderId.value = expandedOrderId.value === orderId ? '' : orderId
|
||||||
|
}
|
||||||
|
|
||||||
|
const getExtraCount = (order: OrderRecord) => {
|
||||||
|
return Math.max(order.items.length - 2, 0)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.c-svg {
|
||||||
|
width: initial;
|
||||||
|
height: initial;
|
||||||
|
}
|
||||||
|
.wardrobe-orders {
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 0 9rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
.orders-toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1.2rem;
|
||||||
|
padding: 3.6rem 0 2.8rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
.orders-toolbar__chip {
|
||||||
|
height: 4rem;
|
||||||
|
min-width: 8rem;
|
||||||
|
padding: 0 3rem;
|
||||||
|
border: 0.1rem solid #232323;
|
||||||
|
border-radius: 2rem;
|
||||||
|
background: #ffffff;
|
||||||
|
font-family: 'KaiseiOpti-Regular';
|
||||||
|
font-size: 1.6rem;
|
||||||
|
color: #585858;
|
||||||
|
font-weight: 400;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&.is-active {
|
||||||
|
background: #232323;
|
||||||
|
border-color: #232323;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.orders-list {
|
||||||
|
padding-bottom: 8rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-card {
|
||||||
|
position: relative;
|
||||||
|
border-bottom: 0.1rem solid #c4c4c4;
|
||||||
|
|
||||||
|
.order-card__toggle {
|
||||||
|
position: absolute;
|
||||||
|
top: 5.8rem;
|
||||||
|
left: 4.2rem;
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
span {
|
||||||
|
display: block;
|
||||||
|
width: 0.9rem;
|
||||||
|
height: 0.9rem;
|
||||||
|
border-right: 0.1rem solid #585858;
|
||||||
|
border-bottom: 0.1rem solid #585858;
|
||||||
|
transform: rotate(-45deg);
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
|
||||||
|
&.is-expanded {
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-card__summary {
|
||||||
|
min-height: 12.4rem;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 25rem minmax(24rem, 1fr) 14rem 12rem 18rem;
|
||||||
|
align-items: center;
|
||||||
|
column-gap: 2rem;
|
||||||
|
padding-left: 9rem;
|
||||||
|
|
||||||
|
.order-card__meta {
|
||||||
|
.order-card__id {
|
||||||
|
font-family: 'KaiseiOpti-Bold';
|
||||||
|
font-size: 2rem;
|
||||||
|
line-height: 3rem;
|
||||||
|
color: #232323;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-card__date {
|
||||||
|
margin: 0.8rem 0 0;
|
||||||
|
font-family: 'KaiseiOpti-Regular';
|
||||||
|
font-size: 1.4rem;
|
||||||
|
color: #808080;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-card__preview {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.order-card__thumb {
|
||||||
|
width: 8rem;
|
||||||
|
height: 10rem;
|
||||||
|
display: block;
|
||||||
|
margin-right: 1.2rem;
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-card__extra {
|
||||||
|
margin-left: 1.2rem;
|
||||||
|
font-family: 'KaiseiOpti-Regular';
|
||||||
|
font-size: 1.4rem;
|
||||||
|
color: #808080;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-card__status {
|
||||||
|
justify-self: start;
|
||||||
|
min-width: 8.8rem;
|
||||||
|
height: 2.4rem;
|
||||||
|
padding: 0 1.6rem;
|
||||||
|
border-radius: 2.4rem;
|
||||||
|
font-family: 'KaiseiOpti-Regular';
|
||||||
|
font-size: 1.4rem;
|
||||||
|
line-height: 2.4rem;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
&.is-paid {
|
||||||
|
background: #e8f2ec;
|
||||||
|
color: #769591;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-unpaid {
|
||||||
|
background: #fef3e2;
|
||||||
|
color: #b48230;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-cancelled {
|
||||||
|
background: #fff2f2;
|
||||||
|
color: #c65f5a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-card__amount {
|
||||||
|
white-space: nowrap;
|
||||||
|
color: #232323;
|
||||||
|
font-family: 'KaiseiOpti-Bold';
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
small {
|
||||||
|
margin-left: 0.4rem;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-card__action {
|
||||||
|
justify-self: center;
|
||||||
|
width: 14rem;
|
||||||
|
height: 3.8rem;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 1rem;
|
||||||
|
border: 0.1rem solid #c4c4c4;
|
||||||
|
background: #f6f6f6;
|
||||||
|
font-family: 'KaiseiOpti-Bold';
|
||||||
|
font-size: 1.6rem;
|
||||||
|
color: #232323;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&.is-primary {
|
||||||
|
width: 16rem;
|
||||||
|
border-color: #232323;
|
||||||
|
background: #232323;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .order-card__details {
|
||||||
|
margin-left: 9rem;
|
||||||
|
padding: 0 2.4rem;
|
||||||
|
background: #fafafa;
|
||||||
|
|
||||||
|
:deep(.sc-item) {
|
||||||
|
--sc-item-img-width: 9.5rem;
|
||||||
|
--sc-item-img-height: 12rem;
|
||||||
|
--sc-item-padding: 1.2rem 2.4rem;
|
||||||
|
--sc-item-content-margin: 0 4rem;
|
||||||
|
--sc-item-title-font-size: 2rem;
|
||||||
|
--sc-item-brand-font-size: 1.4rem;
|
||||||
|
--sc-item-amount-font-size: 2.2rem;
|
||||||
|
--sc-item-currency-font-size: 1.2rem;
|
||||||
|
--sc-item-tag-min-width: 8.8rem;
|
||||||
|
--sc-item-tag-height: 2.4rem;
|
||||||
|
--sc-item-tag-radius: 2.4rem;
|
||||||
|
--sc-item-tag-font-size: 1.4rem;
|
||||||
|
--sc-item-order-amount-width: 12rem;
|
||||||
|
--sc-item-order-action-width: 18rem;
|
||||||
|
--sc-item-order-column-gap: 2rem;
|
||||||
|
--sc-item-order-actions-offset: 4.8rem;
|
||||||
|
border-bottom-color: #e2e2e2;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.sc-item:last-child) {
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
231
src/views/wardrobe/index.vue
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
<template>
|
||||||
|
<div class="wardrobe-page">
|
||||||
|
<div class="wardrobe-hero flex flex-col flex-center">
|
||||||
|
<div class="wardrobe-hero__title">My Wardrobe</div>
|
||||||
|
<div class="wardrobe-hero__subtitle">Your digital pieces, all in one place</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wardrobe-shell">
|
||||||
|
<div class="wardrobe-tabs">
|
||||||
|
<div class="wardrobe-tabs__nav" role="tablist" aria-label="Wardrobe tabs">
|
||||||
|
<button
|
||||||
|
v-for="tab in tabs"
|
||||||
|
:key="tab.key"
|
||||||
|
class="wardrobe-tabs__item"
|
||||||
|
:class="{ 'is-active': activeTab === tab.key }"
|
||||||
|
type="button"
|
||||||
|
role="tab"
|
||||||
|
:aria-selected="activeTab === tab.key"
|
||||||
|
@click="activeTab = tab.key"
|
||||||
|
>
|
||||||
|
{{ tab.label }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="activeTab === 'assets'" class="wardrobe-tabs__sort">
|
||||||
|
<div class="wardrobe-tabs__sort-label">Sort by</div>
|
||||||
|
<el-select v-model="activeSort" placeholder="Select">
|
||||||
|
<el-option
|
||||||
|
v-for="option in sortOptions"
|
||||||
|
:key="option.value"
|
||||||
|
:label="option.label"
|
||||||
|
:value="option.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<component :is="activePanel" class="wardrobe-shell__panel" />
|
||||||
|
</div>
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, shallowRef } from 'vue'
|
||||||
|
import Assets from './Assets.vue'
|
||||||
|
import Orders from './Orders.vue'
|
||||||
|
|
||||||
|
type WardrobeTab = 'assets' | 'orders'
|
||||||
|
|
||||||
|
interface TabItem {
|
||||||
|
key: WardrobeTab
|
||||||
|
label: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SortOption {
|
||||||
|
label: string
|
||||||
|
value: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const tabs: TabItem[] = [
|
||||||
|
{
|
||||||
|
key: 'assets',
|
||||||
|
label: 'Assets'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'orders',
|
||||||
|
label: 'Orders'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const sortOptions: SortOption[] = [
|
||||||
|
{
|
||||||
|
label: 'Default',
|
||||||
|
value: 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Date Added',
|
||||||
|
value: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Selected First',
|
||||||
|
value: 2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const activeTab = shallowRef<WardrobeTab>('assets')
|
||||||
|
const activeSort = shallowRef(1)
|
||||||
|
|
||||||
|
const activePanel = computed(() => {
|
||||||
|
return activeTab.value === 'assets' ? Assets : Orders
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.wardrobe-page {
|
||||||
|
--wardrobe-border-color: #d9d4cd;
|
||||||
|
--wardrobe-text-main: #232323;
|
||||||
|
--wardrobe-text-secondary: #7a746d;
|
||||||
|
--wardrobe-surface: #fffdf8;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: #ffffff;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.wardrobe-hero {
|
||||||
|
height: 14.8rem;
|
||||||
|
// background-color: #f5f5f5;
|
||||||
|
background: url('@/assets/images/background.png') no-repeat;
|
||||||
|
background-size: cover;
|
||||||
|
position: relative;
|
||||||
|
// &::before {
|
||||||
|
// position: absolute;
|
||||||
|
// top: 0;
|
||||||
|
// right: 0;
|
||||||
|
// bottom: 0;
|
||||||
|
// left: 0;
|
||||||
|
// background-color: rgba(0, 0, 0, 0.2);
|
||||||
|
// }
|
||||||
|
.wardrobe-hero__title {
|
||||||
|
margin: 0;
|
||||||
|
font-family: 'KaiseiOpti-Bold';
|
||||||
|
font-size: 4rem;
|
||||||
|
color: #232323;
|
||||||
|
line-height: 3.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wardrobe-hero__subtitle {
|
||||||
|
margin-top: 1rem;
|
||||||
|
font-family: 'KaiseiOpti-Regular';
|
||||||
|
font-size: 1.4rem;
|
||||||
|
line-height: 2.4rem;
|
||||||
|
color: #585858;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .wardrobe-shell {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
> .wardrobe-tabs {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 2rem;
|
||||||
|
padding: 0 9rem;
|
||||||
|
border-bottom: 0.1rem solid var(--wardrobe-border-color);
|
||||||
|
background: #ffffff;
|
||||||
|
|
||||||
|
> .wardrobe-tabs__nav {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
> .wardrobe-tabs__item {
|
||||||
|
position: relative;
|
||||||
|
height: 6rem;
|
||||||
|
padding: 0 1.8rem;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
font-family: 'KaiseiOpti-Regular';
|
||||||
|
font-size: 1.4rem;
|
||||||
|
line-height: 1;
|
||||||
|
color: #7d766f;
|
||||||
|
cursor: pointer;
|
||||||
|
width: 13.9rem;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 1.8rem;
|
||||||
|
right: 1.8rem;
|
||||||
|
bottom: -0.1rem;
|
||||||
|
height: 0.2rem;
|
||||||
|
background: transparent;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-active {
|
||||||
|
font-family: 'KaiseiOpti-Bold';
|
||||||
|
color: var(--wardrobe-text-main);
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
background: var(--wardrobe-text-main);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .wardrobe-tabs__sort {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
> .wardrobe-tabs__sort-label {
|
||||||
|
font-family: 'KaiseiOpti-Regular';
|
||||||
|
font-size: 1.4rem;
|
||||||
|
line-height: 2.4rem;
|
||||||
|
color: #7d766f;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-select) {
|
||||||
|
width: 13rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-select__wrapper) {
|
||||||
|
min-height: 3.6rem;
|
||||||
|
padding: 0 1.2rem;
|
||||||
|
background: #ffffff;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-select__selected-item) {
|
||||||
|
font-family: 'KaiseiOpti-Regular';
|
||||||
|
font-size: 1.4rem;
|
||||||
|
color: #232323;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .wardrobe-shell__panel {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||