Compare commits

...

50 Commits

Author SHA1 Message Date
X1627315083@163.com
8c8ec7846d 调整添加购物车逻辑 2026-05-12 17:02:49 +08:00
X1627315083@163.com
8d441766c5 fix 2026-05-12 14:13:56 +08:00
X1627315083@163.com
bf907a1378 digital空状态 2026-05-12 13:35:53 +08:00
李志鹏
de6295f2af Merge branch 'main' of http://18.167.251.121:10003/aidlab/Aida_Purchaser_Front 2026-05-12 13:32:48 +08:00
李志鹏
3e0a7b8928 导航栏激活 2026-05-12 13:32:47 +08:00
X1627315083@163.com
09909552bc digital 过滤调整 2026-05-12 13:27:30 +08:00
李志鹏
baf57515c0 登录还原 2026-05-12 11:10:42 +08:00
X1627315083@163.com
b8c844363c brand页面交互调整 2026-05-11 16:16:59 +08:00
李志鹏
87b071c319 22 2026-05-11 14:34:37 +08:00
李志鹏
bdc824e1f6 11 2026-05-11 14:22:52 +08:00
李志鹏
1c7b2d32a6 空列表 2026-05-11 14:21:42 +08:00
李志鹏
cffd554365 Merge branch 'main' of http://18.167.251.121:10003/aidlab/Aida_Purchaser_Front 2026-05-11 13:56:12 +08:00
李志鹏
33043eedf1 home修改 2026-05-11 13:56:10 +08:00
ce35f788ca feat: 商品item组件 2026-05-11 13:15:31 +08:00
5bbdeb6236 Merge branch 'main' of ssh://18.167.251.121:10002/aidlab/Aida_Purchaser_Front 2026-05-11 11:29:00 +08:00
93813f7b56 chore: setting页面拆分 2026-05-11 11:28:59 +08:00
李志鹏
51e6933f9f Merge branch 'main' of http://18.167.251.121:10003/aidlab/Aida_Purchaser_Front 2026-05-11 10:51:10 +08:00
李志鹏
c0b4742966 11 2026-05-11 10:51:00 +08:00
f76c9ed828 feat: 设置页去掉社媒填写 2026-05-11 10:39:34 +08:00
d50e3781db feat: 设置页i18n 2026-05-05 09:59:44 +08:00
e48e369ef1 feat: 验证码输入弹窗 2026-05-04 17:06:04 +08:00
bb8344b27a feat: account页面 2026-05-04 15:53:13 +08:00
b538853800 feat: 页脚 2026-04-24 14:50:28 +08:00
f772e3d250 style: 订单子列表样式 2026-04-24 14:36:52 +08:00
f5a7ad51f0 Merge branch 'main' of ssh://18.167.251.121:10002/aidlab/Aida_Purchaser_Front 2026-04-24 14:13:15 +08:00
8df8618f40 feat: wardrobe orders 2026-04-24 14:04:01 +08:00
李志鹏
f355835819 Merge branch 'main' of http://18.167.251.121:10003/aidlab/Aida_Purchaser_Front 2026-04-24 09:38:25 +08:00
李志鹏
42c2817c2f 分离 2026-04-24 09:38:24 +08:00
9bf0211e78 Merge branch 'main' of ssh://18.167.251.121:10002/aidlab/Aida_Purchaser_Front 2026-04-23 17:29:17 +08:00
281272c9f9 feat: wardrobe assets页面 2026-04-23 17:29:15 +08:00
李志鹏
71cfef996d 更改字节转换方法 2026-04-23 17:03:45 +08:00
李志鹏
09125378b6 gogogo 2026-04-23 16:35:07 +08:00
李志鹏
e8da956543 Merge branch 'main' of http://18.167.251.121:10003/aidlab/Aida_Purchaser_Front 2026-04-23 16:11:14 +08:00
李志鹏
ad81feaf81 购物车 2026-04-23 16:11:13 +08:00
254f61e524 Merge branch 'main' of ssh://18.167.251.121:10002/aidlab/Aida_Purchaser_Front 2026-04-23 15:20:46 +08:00
3512d9ae87 feat: wardrobe页面 2026-04-23 15:20:22 +08:00
李志鹏
98e2548f01 11 2026-04-23 15:18:30 +08:00
李志鹏
a6484782a5 Merge branch 'main' of http://18.167.251.121:10003/aidlab/Aida_Purchaser_Front 2026-04-23 15:15:45 +08:00
李志鹏
017d052cd8 购物车 2026-04-23 15:15:43 +08:00
X1627315083@163.com
bd35b3f89f Merge branch 'main' of ssh://18.167.251.121:10002/aidlab/Aida_Purchaser_Front 2026-04-23 14:30:11 +08:00
X1627315083@163.com
66b019eb2a fix 2026-04-23 14:30:09 +08:00
李志鹏
4c2edb53e4 Merge branch 'main' of http://18.167.251.121:10003/aidlab/Aida_Purchaser_Front 2026-04-23 14:28:27 +08:00
李志鹏
829f164833 购物车 2026-04-23 14:28:26 +08:00
X1627315083@163.com
45298e5f23 digital item 页面 2026-04-23 14:08:35 +08:00
李志鹏
7ca69021c4 Merge branch 'main' of http://18.167.251.121:10003/aidlab/Aida_Purchaser_Front 2026-04-23 11:49:00 +08:00
李志鹏
b0ee5a0783 购物车 2026-04-23 11:48:22 +08:00
5c931317f0 Merge branch 'main' of ssh://18.167.251.121:10002/aidlab/Aida_Purchaser_Front 2026-04-23 09:39:14 +08:00
751cd1aa1f feat: notification页面 2026-04-23 09:39:12 +08:00
X1627315083@163.com
80eaa803dd Merge branch 'main' of ssh://18.167.251.121:10002/aidlab/Aida_Purchaser_Front 2026-04-22 16:29:41 +08:00
X1627315083@163.com
7b5dcdacd7 digitalItem页面 2026-04-22 16:29:22 +08:00
101 changed files with 6918 additions and 1266 deletions

View File

@@ -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>

View File

@@ -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">

View File

@@ -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;
}

View File

@@ -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 {
@@ -165,4 +168,13 @@ button[custom="black"] {
--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;
} }

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 713 KiB

After

Width:  |  Height:  |  Size: 597 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 713 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

After

Width:  |  Height:  |  Size: 808 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 615 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 851 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 652 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 688 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 569 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 KiB

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 317 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 B

View File

@@ -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';
</style> 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>

View 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>

View File

@@ -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'
}
}
} }

View File

@@ -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: '英国'
}
}
} }

View File

@@ -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',

View File

@@ -53,9 +53,9 @@ export function generateUUID(): string {
if (typeof crypto !== 'undefined' && crypto.randomUUID) { if (typeof crypto !== 'undefined' && crypto.randomUUID) {
return crypto.randomUUID() return crypto.randomUUID()
} }
// 备用方案手动生成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
View 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>

View 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>

View File

@@ -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%;
}
} }
} }
} }

View File

@@ -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;
} }
} }
} }

View 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>

View File

@@ -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;
} }

View 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>

View File

@@ -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"> Were 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;
line-height: 100%;
color: #585858;
} }
100% { }
transform: translateY(0%); > .button{
padding: 0 4.5rem;
line-height: 5.1rem;
background-color: #1B1B1B;
color: #fff;
margin-top: 4rem;
font-weight: 700;
font-size: 2rem;
letter-spacing: -0.4px;
cursor: pointer;
> a{
color: #fff;
text-decoration: none;
} }
} }
> div{
font-family: 'KaiseiOpti-Regular';
font-weight: 400;
font-size: 1.4rem;
line-height: 100%;
text-align: center;
margin-bottom: 1.5rem;
white-space: nowrap;
}
} }
.banner{ .banner{
width: 100%; width: 100%;
position: absolute;
z-index: -1; z-index: -1;
} }
} }

View 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>

View 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>

View 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>

View 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>

View File

@@ -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'

View File

@@ -0,0 +1,51 @@
<template>
<section class="section-designer">
<div class="title">Designer Community</div>
<div class="tip">
Discover the designers shaping AiDAs 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>

View File

@@ -1,130 +0,0 @@
<template>
<section class="section-designers">
<div class="title">Popular Designers</div>
<div class="tip">
Discover the designers shaping AiDAs 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 ones 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>

View File

@@ -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">

View File

@@ -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 =
'Were 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;
} }
} }
} }

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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 {

View 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>

View 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>

View 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>

View File

@@ -0,0 +1,8 @@
export interface NotificationRecord {
id: string
title: string
date: string
content: string
isUnread: boolean
isExpanded: boolean
}

View 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>

View 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>

View File

@@ -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',

View 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>

View 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>

View 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>

View 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>

View File

@@ -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 &amp; 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;
}
} }
} }

View 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
}

View 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
}
}

View 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>

View 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>

View 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>

View 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>

View 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>

View 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 }}&nbsp;&nbsp;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>

View 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>

View 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>

View 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>

View 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>

Some files were not shown because too many files have changed in this diff Show More