Compare commits

...

16 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
62 changed files with 2490 additions and 1481 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" />
<link rel="stylesheet" href="/fonts/index.css">
<title>Lane Crawford</title>
<title>Stylish Parade</title>
<!-- Open Graph / WhatsApp share metadata -->
<meta property="og:title" content="Lane Crawford" />
<meta property="og:description" content="create and share looks from the Lane Crawford creation gallery." />
<meta property="og:title" content="Stylish Parade" />
<meta property="og:description" content="create and share looks from the Stylish Parade creation gallery." />
<meta property="og:image" content="" />
<meta property="og:url" content="https://www.lc.aida.com.hk" />
<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" />
</head>
<body>

View File

@@ -122,11 +122,11 @@ button[custom="white"] {
height: 5rem;
padding: 0 1rem;
border-radius: 0;
border: none;
font-family: KaiseiOpti-Bold;
font-size: var(--button-font-size, 2rem);
color: var(--button-color, #232323);
background: var(--button-bgcolor, #fff);
border: var(--button-border, none);
cursor: pointer;
}
button[custom]:active,
@@ -141,6 +141,11 @@ button[custom="black"] {
--button-click-color: #fff;
--button-font-size: 1.6rem;
}
.el-select-dropdown__item {
padding: 0 2rem !important;
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

@@ -149,11 +149,11 @@ button[custom="white"] {
height: 5rem;
padding: 0 1rem;
border-radius: 0;
border: none;
font-family: KaiseiOpti-Bold;
font-size: var(--button-font-size, 2rem);
color: var(--button-color, #232323);
background: var(--button-bgcolor, #fff);
border: var(--button-border, none);
cursor: pointer;
&:active {
@@ -168,4 +168,13 @@ button[custom="black"] {
--button-click-bgcolor: #333;
--button-click-color: #fff;
--button-font-size: 1.6rem;
}
button[custom="black-box"] {
--button-bgcolor: transparent;
--button-color: #232323;
--button-border: 0.2rem solid #979797;
--button-click-bgcolor: #979797;
--button-click-color: #fff;
--button-font-size: 1.6rem;
}

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="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,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: 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.

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

View File

@@ -16,6 +16,10 @@ const props = defineProps({
download: {
type: Boolean,
default: false
},
showPrice: {
type: Boolean,
default: true
}
})
const emit = defineEmits(['addShopping', 'openDetail', 'download'])
@@ -43,7 +47,7 @@ const {} = toRefs(data)
<div class="name">
{{ props.name }}
</div>
<div class="price" :class="{ 'is-download': download }">
<div class="price" :class="{ 'is-download': download }" v-if="props.showPrice">
{{ props.price }}
</div>
</div>

View File

@@ -14,11 +14,23 @@ const emit = defineEmits([
'update:selected'
])
const checkList = computed(()=>{
return [props.selected]
if(props.selected[0] === ''){
return props.list.map(item => item.value)
}else{
return [...props.selected]
}
})
const handleChange = (val) => {
if (val.length > 1) {
emit('update:selected', val[val.length - 1])
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({
@@ -31,6 +43,14 @@ 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"
@@ -42,6 +62,9 @@ const {} = toRefs(data);
</el-checkbox-group>
</template>
<style lang="less" scoped>
.all{
margin-bottom: 1.2rem;
}
.el-checkbox-group{
display: flex;
flex-direction: column;

View File

@@ -2,8 +2,7 @@ export default {
Login: {
login: 'Log in',
register: 'Register',
loginTo: 'Log on to <span>FiDA</span>',
loginTitle: 'A multi-agent canvas for rapid, trend driven design iteration.',
loginTip: 'Platform integrated with AiDA.<br />AiDA account login required.',
name: 'Name',
email: 'Email',
password: 'Password',

View File

@@ -2,9 +2,7 @@ export default {
Login: {
login: '登录',
register: '注册',
signUp: '注册',
loginTo: '登录到 <span>FiDA</span',
loginTitle: '一个多智能体画布,用于快速、趋势驱动的设计迭代。',
loginTip: '与 AiDA 集成的平台。<br />需要登录 AiDA 账户。',
name: '姓名',
email: '邮箱',
password: '密码',

View File

@@ -23,6 +23,11 @@ const router = createRouter({
name: 'brand',
component: () => import('../views/brand/index.vue')
},
{
path: '/brand/:id',
name: 'brandDetail',
component: () => import('../views/brandDetail/index.vue')
},
{
path: '/digitalItem',
name: 'digitalItem',

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,9 +1,11 @@
<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'
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 emit = defineEmits([
@@ -11,12 +13,96 @@ import myEvent from '@/utils/myEvent'
const router = useRouter()
let data = reactive({
})
const addShopping = (item) => {
myEvent.emit('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 openDetail = (item) => {
router.push({name: 'digitalDetail', params: {id: item.id}})
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(()=>{
})
onUnmounted(()=>{
@@ -26,15 +112,52 @@ const {} = toRefs(data);
</script>
<template>
<div class="brand">
<div class="header-img">
<div class="header-img" :class="{'active': searchBrand.length > 0}">
<img src="@/assets/images/brand/brandBg.png" alt="">
<div class="text-box">
<div class="title">Brand</div>
<span>Every brand, every story discover who's behind the collections.</span>
</div>
</div>
<div class="content">
<div class="merchant-info">
<MerchantInfo></MerchantInfo>
<div class="input">
<input type="text" v-model="searchBrand" @input="changeSearchBrand" placeholder="Search brand">
<div class="icon">
<SvgIcon name="brand-search" size="32" />
</div>
</div>
<div class="commodity-list">
<CommodityList @addShopping="addShopping" @openDetail="openDetail"></CommodityList>
<div class="merchantList" v-if="searchBrand.length > 0">
<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>
<Footer></Footer>
@@ -46,34 +169,151 @@ const {} = toRefs(data);
height: 100%;
position: relative;
overflow-y: auto;
display: flex;
flex-direction: column;
.header-img{
width: 100%;
position: relative;
height: 34.4rem;
transition: all .3s;
&.active{
height: 14.7rem;
}
>img{
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;
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;
flex-direction: column;
align-items: center;
flex: 1;
overflow: hidden;
> .input{
width: 66.6rem;
display: flex;
border-bottom: 2px solid #232323;
padding: 1.4rem 1.4rem 1.4rem 2.4rem;
background-color: #f9f9f9;
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;
border-left: 0.5px solid #585858;
border-right: 0.5px solid #585858;
margin-right: 9rem;
overflow-y: auto;
gap: 3.2rem;
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,
title: "Windswept Burden",
price: "$100.00",
},{
url: img,
title: "Windswept Burden",
price: "$100.00",
},
}
])
const type = ref('All')
const addShopping = (item) => {
@@ -128,41 +124,26 @@ const {} = toRefs(data);
}
}
.list{
border-top: 0.5px solid #585858;
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(3, 1fr);
overflow-y: auto;
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;
}
.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;
z-index: 1;
}
/* 移除最后一列的右边框 */
.item:nth-child(3n)::before {
display: none;
border-right: 0.5px solid #585858;
margin-right: -1px;
margin-bottom: -1px;
}
}
}

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,6 +4,7 @@ import img from "@/assets/images/collectionStory/Rectangle.png";
import coreConcept from "./coreConcept.vue";
import inspiration from "./inspiration.vue";
import feelingWithAiDA from "./feelingWithAiDA.vue";
import joinUs from "./join-us.vue";
import CommodityItem from "@/components/CommodityItem.vue";
//const props = defineProps({
//})
@@ -30,6 +31,10 @@ const list = ref([
const addShopping = (item) => {
emit('addShopping', item)
}
const openCodeCreate = () => {
window.open('https://code-create.com.hk/', '_blank')
}
onMounted(()=>{
})
onUnmounted(()=>{
@@ -41,21 +46,22 @@ const {} = toRefs(data);
<div class="detail">
<div class="left">
<div class="personal">
<img :src="img" alt="">
<img src="@/assets/images/collectionStory/code-create.png" alt="">
<div class="name">
<span>Lian Su</span>
<div class="icon">
<span>Code-Create</span>
<div class="icon" @click="openCodeCreate">
<SvgIcon name="share" size="24" />
</div>
</div>
</div>
</div>
<div class="center">
<coreConcept ></coreConcept>
<joinUs></joinUs>
<!-- <coreConcept ></coreConcept>
<div class="line"></div>
<inspiration ></inspiration>
<div class="line"></div>
<feelingWithAiDA ></feelingWithAiDA>
<feelingWithAiDA ></feelingWithAiDA> -->
</div>
<div class="right">
<div class="item" v-for="item in list" :key="item.url">
@@ -67,10 +73,11 @@ const {} = toRefs(data);
<style lang="less" scoped>
.detail{
width: 100%;
height: auto;
position: relative;
display: flex;
height: calc(100vh - var(--header-height) - var(--footer-height));
align-items: flex-start;
overflow: hidden;
> div{
// height: 100%;
}
@@ -93,6 +100,9 @@ const {} = toRefs(data);
display: flex;
gap: .4rem;
align-items: center;
> .icon{
cursor: pointer;
}
> span{
font-family: 'KaiseiOpti-Bold';
font-weight: 700;
@@ -110,8 +120,9 @@ const {} = toRefs(data);
border-right: 0.5px solid #585858;
// overflow-y: auto;
overflow: hidden;
// height: 100%;
height: auto;
height: 100%;
// height: auto;
position: relative;
.line{
border: 0.5px solid #58585899;
width: 100%;
@@ -123,14 +134,15 @@ const {} = toRefs(data);
> .right{
width: 25.4rem;
padding-top: 6rem;
position: sticky;
// position: sticky;
top: 0;
height: calc(100vh - var(--header-height));
// height: calc(100vh - var(--header-height));
display: flex;
flex-direction: column;
align-items: center;
gap: 4rem;
overflow-y: auto;
height: 100%;
&::-webkit-scrollbar{
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

@@ -28,24 +28,15 @@ const {} = toRefs(data);
</div>
<div class="title-content">
<div class="title-box">
<div class="left">
<div class="title">
Windswept Burden
</div>
<div class="info">
Publish Date: 24th Nov 2025
</div>
<div class="title">
Were Seeking
</div>
<div class="right">
<div class="info">
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 class="info">
Fashion Voice Worth Featuring.
</div>
</div>
<div class="scrolling-learn-more">
<div>Scrolling Learn More</div>
<SvgIcon name="collectionStory-scrollingLearnMore" size="48" />
<div class="button">
<a href="mailto:info@code-create.com.hk">Contact Us if Interested</a>
</div>
</div>
</div>
@@ -73,7 +64,7 @@ const {} = toRefs(data);
display: flex;
align-items: center;
gap: .8rem;
color: #fff;
color: #000;
cursor: pointer;
> .text{
font-size: 2rem;
@@ -83,73 +74,46 @@ const {} = toRefs(data);
}
> .title-content{
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;
bottom: 2.1rem;
left: 50%;
transform: translateX(-50%);
padding: 0 6.7rem;
margin-top: 11.5rem;
display: flex;
flex-direction: column;
color: #fff;
animation: scroll 3s linear infinite;
@keyframes scroll {
0% {
transform: translateY(0);
align-items: flex-start;
> .title-box{
display: flex;
flex-direction: column;
> .title{
font-size: 6.5rem;
line-height: 100%;
font-weight: 500;
color: #585858;
}
50% {
transform: translateY(-20%);
> .info{
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{
width: 100%;
position: absolute;
z-index: -1;
}
}

View File

@@ -62,10 +62,7 @@ const {} = toRefs(data);
<div class="detail">
<div class="name">Roaming Clouds</div>
<div class="release-time">
<div class="icon">
<svg-icon name="digital-document" size="24"></svg-icon>
</div>
<span>12mb | Release in Feb 26, 2026</span>
<span>Release in Feb 26, 2026</span>
</div>
</div>
</div>
@@ -235,9 +232,7 @@ const {} = toRefs(data);
color: #585858;
display: flex;
align-items: center;
> span{
margin-left: 1rem;
}
}
}
}

View File

@@ -77,42 +77,24 @@ const {} = toRefs(data);
.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(3, 1fr);
overflow-y: auto;
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;
--commodity-marginBottom: 2rem;
--commodity-name-fontSize: 2rem;
--commodity-name-marginBottom: .8rem;
--commodity-price-fontSize: 1.6rem;
}
.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;
z-index: 1;
}
/* 移除最后一列的右边框 */
.item:nth-child(3n)::before {
display: none;
border-right: 0.5px solid #585858;
margin-right: -1px;
margin-bottom: -1px;
}
}
}

View File

@@ -3,6 +3,8 @@ 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'
@@ -16,6 +18,20 @@ 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
@@ -42,12 +58,36 @@ const {} = toRefs(data);
<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 @addShopping="addShopping" @openDetail="openDetail"></CommodityList>
<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>
@@ -89,12 +129,67 @@ const {} = toRefs(data);
}
}
}
.content{
> .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;
// align-items: flex-start;
border-top: 0.5px solid #585858;
margin-top: 6rem;
.merchant-info{
width: 38.5rem;
padding-left: 10.2rem;
@@ -112,6 +207,10 @@ const {} = toRefs(data);
border-left: 0.5px solid #585858;
border-right: 0.5px solid #585858;
margin-right: 9rem;
display: flex;
.null{
flex: 1;
}
}
}
}

View File

@@ -7,10 +7,6 @@ import { ref, onMounted, onUnmounted, reactive, toRefs } from "vue";
let data = reactive({
})
const categoriesList = ref([
{
label: 'All',
value: 'All'
},
{
label: 'Outwear',
value: 'Outwear'
@@ -37,10 +33,6 @@ const categoriesList = ref([
},
]);
const genderList = ref([
{
label: 'All',
value: 'All'
},
{
label: 'Male',
value: 'Male'
@@ -50,12 +42,12 @@ const genderList = ref([
value: 'Female'
},
])
const categories = ref('All')
const gender = ref('All')
const categories = ref([''])
const gender = ref([''])
const clearFilters = () => {
categories.value = 'All'
gender.value = 'All'
categories.value = ['']
gender.value = ['']
}
onMounted(()=>{
})

View File

@@ -1,7 +1,7 @@
<template>
<div class="home-index">
<section-index />
<section-designers />
<section-designer />
<section-design />
<section-digital-items1 />
<section-digital-items2 />
@@ -12,7 +12,7 @@
<script setup lang="ts">
import { computed } from '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 SectionDigitalItems1 from './section-digital-items1.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="logos">
<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" />
</div>
<div class="tip">

View File

@@ -1,74 +1,58 @@
<template>
<section class="section-index bgw">
<img src="@/assets/images/home/bg.jpg" class="bg" />
<div class="shade-1"></div>
<div class="shade-2"></div>
<img src="@/assets/images/home/bg.png" class="bg" />
<div class="content">
<div class="title">Windswept Burden</div>
<div class="tip">We are spiritual nomads carrying<br />what wind cannot take.</div>
<button custom>View More</button>
<div class="aida-logo"><img src="@/assets/images/logos/aida.png" /></div>
<p class="tip">
What you wear is how you present yourself to the world, especially today, when human
contacts are so quick. Fashion is instant language
</p>
<p class="tip">I firmly believe that with the right footwear one can rule the world.</p>
<div class="title" v-html="title"></div>
<div class="tip">
Discover collections through the stories behind their creation. A curated space connecting
designers, narratives, and fashion commerce.
</div>
<button custom="black-box" @click="handleClickArrow">
<svg-icon name="arrow_right" size="34" />
</button>
</div>
</section>
</template>
<script setup lang="ts">
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>
<style lang="less">
.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 {
top: 16rem;
top: 13.7rem;
left: 9rem;
color: #fff;
color: #232323;
> .title {
font-family: KaiseiOpti-Bold;
font-size: 6rem;
font-size: 7rem;
margin-bottom: 3rem;
}
> div.tip {
font-family: KaiseiOpti-Regular;
font-size: 3.2rem;
line-height: 4.3rem;
}
> button {
margin-top: 11.6rem;
margin-bottom: 13.9rem;
}
> .aida-logo {
margin-bottom: 3rem;
> img {
width: auto;
height: 5rem;
span {
font-family: KaiseiOpti-Medium;
&.small {
font-size: 6rem;
}
}
}
> p.tip {
font-family: KaiseiOpti-Regular;
font-size: 1.2rem;
line-height: 2rem;
color: #ededed;
> .tip {
width: 50rem;
font-size: 1.8rem;
line-height: 2.6rem;
color: #585858;
}
> button {
margin-top: 12rem;
min-width: 0;
width: 8rem;
height: 8rem;
}
}
}

View File

@@ -8,7 +8,9 @@
v-for="v in navList1"
:key="v.path"
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)"
>
<span>{{ v.name }}</span>
@@ -19,7 +21,7 @@
class="icon"
v-for="v in navList2"
:key="v.path"
:class="{ active: activePath === v.path }"
:class="{ active: new RegExp(`^${v.path}`).test(activePath) }"
@click="onNavItemClick(v.path)"
>
<svg-icon :name="activePath === v.path ? v.active_icon : v.icon" size="22" />
@@ -62,6 +64,10 @@
</div>
</template>
</el-popover>
<div class="language" @click="onLanguageClick">
<span :class="{ active: locale === 'CHINESE_SIMPLIFIED' }"></span> /
<span :class="{ active: locale === 'ENGLISH' }">ENG</span>
</div>
</div>
</div>
</template>
@@ -70,6 +76,8 @@
import { computed, ref } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import myEvent from '@/utils/myEvent'
import { useI18n } from 'vue-i18n'
const { t, locale } = useI18n()
const router = useRouter()
const route = useRoute()
const activePath = computed(() => route.path)
@@ -131,6 +139,10 @@
hideProfilePopover()
console.log('logout')
}
const onLanguageClick = () => {
locale.value = locale.value === 'ENGLISH' ? 'CHINESE_SIMPLIFIED' : 'ENGLISH'
localStorage.setItem('language', locale.value)
}
</script>
<style lang="less" scoped>
@@ -159,7 +171,6 @@
height: 2.4rem;
}
> .login {
font-family: Kaisei Opti;
font-size: 1.6rem;
}
> .profile {
@@ -168,6 +179,15 @@
border-radius: 50%;
background: #f5f5f5;
}
> .language {
font-family: KaiseiOpti-Regular;
font-size: 1.6rem;
color: #c2c2c2;
> .active {
color: #232323;
font-family: KaiseiOpti-Medium;
}
}
}
> .center,
> .right {
@@ -195,7 +215,6 @@
font-size: 1.6rem;
color: #232323;
border-bottom: 0.1rem solid transparent;
font-family: Kaisei Opti;
}
&.active {
> span {

View File

@@ -66,7 +66,7 @@ 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: ReturnType<typeof window.setInterval> | null = null
let resendCountdownTimer: number | null = null
const formattedResendCountdown = computed(
() => `00:${String(resendCountdown.value).padStart(2, '0')}`

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">
<button
v-for="item in options"
:key="item.value"
:key="String(item.value)"
type="button"
:class="[
'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,240 +1,59 @@
<template>
<div class="setting-wrapper mini-scrollbar">
<div class="banner flex flex-center flex-col">
<div class="banner">
<div class="title">{{ t('Settings.title') }}</div>
<div class="slogan">{{ t('Settings.slogan') }}</div>
</div>
<div class="setting-content">
<div class="setting-item flex">
<div class="label-container">
<div class="label">{{ t('Settings.profile.title') }}</div>
<div class="label-desc">
{{ t('Settings.profile.description') }}
</div>
</div>
<div class="context-container">
<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">{{ 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
v-model="draftData.firstName"
:placeholder="t('Settings.profile.firstNamePlaceholder')"
/>
</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
v-model="draftData.lastName"
:placeholder="t('Settings.profile.lastNamePlaceholder')"
/>
</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
v-model="draftData.username"
:placeholder="t('Settings.profile.usernamePlaceholder')"
/>
</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="roleModel" :options="roleList" />
</div>
</div>
<div class="read-tip">{{ t('Settings.profile.roleTip') }}</div>
</div>
</div>
</div>
<ProfileSection
v-model:first-name="draftData.firstName"
v-model:last-name="draftData.lastName"
v-model:username="draftData.username"
v-model:role-model="roleModel"
:display-data="displayData"
:full-name="fullName"
:is-editing="isEditing"
:role-options="roleList"
/>
<div class="gap" />
<div class="setting-item flex">
<div class="label-container">
<div class="label">{{ t('Settings.security.title') }}</div>
<div class="label-desc">{{ t('Settings.security.description') }}</div>
</div>
<div class="context-container security-container">
<div class="inner-divider" />
<div class="security-row">
<div class="security-inline-row flex align-center">
<div class="security-label inline">{{ t('Settings.security.email') }}</div>
<div class="security-static flex-1">{{ displayData.email }}</div>
<button
v-show="isEditing"
type="button"
class="small-btn"
@click="resetSecurityEmail"
>
{{ 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 align-center">
<el-input
v-model="securityDraft.newEmail"
:placeholder="t('Settings.security.newEmailPlaceholder')"
/>
<button
type="button"
class="verify-btn"
:class="{ verified: isEmailVerified }"
@click="handleVerifyEmail"
>
{{
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 flex align-center">
<div class="security-label inline">{{ t('Settings.security.password') }}</div>
<div class="security-static password-mask flex-1">.........</div>
<button
v-show="isEditing"
type="button"
class="small-btn"
@click="resetSecurityPassword"
>
{{ 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
v-model="securityDraft.newPassword"
type="password"
show-password
:placeholder="t('Settings.security.newPasswordPlaceholder')"
/>
</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
v-model="securityDraft.currentPassword"
type="password"
show-password
:placeholder="t('Settings.security.currentPasswordPlaceholder')"
/>
</div>
</div>
<div class="inner-divider" />
</div>
</div>
<SecuritySection
v-model:new-email="securityDraft.newEmail"
v-model:new-password="securityDraft.newPassword"
v-model:current-password="securityDraft.currentPassword"
:email="displayData.email"
:is-editing="isEditing"
:is-email-verified="isEmailVerified"
@reset-email="resetSecurityEmail"
@reset-password="resetSecurityPassword"
@verify-email="handleVerifyEmail"
/>
<div class="gap" />
<div class="setting-item flex">
<div class="label-container">
<div class="label">{{ t('Settings.region.title') }}</div>
<div class="label-desc">{{ t('Settings.region.description') }}</div>
</div>
<div class="context-container region-container">
<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
v-model="draftData.language"
:placeholder="t('Settings.region.selectLanguage')"
>
<el-option
v-for="item in languageList"
: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
v-model="draftData.region"
:placeholder="t('Settings.region.selectRegion')"
>
<el-option
v-for="item in regionList"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</div>
</div>
</div>
</div>
<RegionSection
v-model:language="draftData.language"
v-model:region="draftData.region"
:display-language-label="displayLanguageLabel"
:display-region-label="displayRegionLabel"
:is-editing="isEditing"
:language-options="languageList"
:region-options="regionList"
/>
<div class="gap bottom-gap" />
<div class="action-container flex">
<template v-if="isEditing">
<button type="button" class="primary-btn" :disabled="saving" @click="handleSave">
{{ saving ? t('Settings.buttons.saving') : t('Settings.buttons.saveChange') }}
</button>
<button type="button" class="secondary-btn" :disabled="saving" @click="handleDiscard">
{{ t('Settings.buttons.discard') }}
</button>
</template>
<template v-else>
<button type="button" class="primary-btn edit-btn" @click="handleEdit">
{{ t('Settings.buttons.edit') }}
</button>
</template>
</div>
<SettingsActions
:is-editing="isEditing"
:saving="saving"
@edit="handleEdit"
@save="handleSave"
@discard="handleDiscard"
/>
</div>
<Footer />
<EmailVerificationDialog
@@ -249,288 +68,42 @@
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { useI18n } from 'vue-i18n'
import EmailVerificationDialog from './components/EmailVerificationDialog.vue'
import Radio from './components/Radio.vue'
const roleValues = [
'fashionEnthusiast',
'contentCreator',
'student',
'retailBuyer',
'fashionDesigner',
'brandBusiness',
'prCommunications',
'stylist',
'graphicDesigner',
'artist3d',
'other'
] as const
const languageValues = ['english', 'chinese'] as const
const regionValues = ['hongKongSar', 'mainlandChina', 'singapore', 'unitedKingdom'] as const
type RoleValue = (typeof roleValues)[number]
type LanguageValue = (typeof languageValues)[number]
type RegionValue = (typeof regionValues)[number]
interface SettingsData {
firstName: string
lastName: string
email: string
username: string
role: RoleValue[]
language: LanguageValue
region: RegionValue
}
interface SecurityDraft {
newEmail: string
newPassword: string
currentPassword: string
}
import ProfileSection from './components/ProfileSection.vue'
import RegionSection from './components/RegionSection.vue'
import SecuritySection from './components/SecuritySection.vue'
import SettingsActions from './components/SettingsActions.vue'
import { useSettingsForm } from './useSettingsForm'
const { t, locale } = useI18n({ useScope: 'global' })
const languageLocaleMap: Record<LanguageValue, 'ENGLISH' | 'CHINESE_SIMPLIFIED'> = {
english: 'ENGLISH',
chinese: 'CHINESE_SIMPLIFIED'
}
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 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: ''
})
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 isVerificationDialogVisible = ref(false)
const verificationTargetEmail = ref('')
const verifiedEmail = ref('')
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({
get: () => displayData.value.role,
set: (value: RoleValue[]) => {
if (isEditing.value) {
draftData.value.role = value
return
}
sourceData.value.role = value
}
})
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
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 = () => {
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()
}
}
)
const {
draftData,
securityDraft,
isEditing,
saving,
isVerificationDialogVisible,
verificationTargetEmail,
roleList,
languageList,
regionList,
displayData,
isEmailVerified,
displayLanguageLabel,
displayRegionLabel,
fullName,
roleModel,
handleEdit,
handleDiscard,
handleSave,
resetSecurityEmail,
resetSecurityPassword,
handleVerifyEmail,
handleSendVerifyCode,
handleVerificationSubmit,
closeVerificationDialog
} = useSettingsForm({ t, locale })
</script>
<style lang="less" scoped>
@@ -538,362 +111,45 @@ watch(
height: 100%;
overflow-y: auto;
background: #ffffff;
}
.field-text() {
font-family: 'KaiseiOpti-Regular';
font-size: 1.6rem;
line-height: 2.4rem;
color: #232323;
}
.banner {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 14.8rem;
row-gap: 1.2rem;
background: linear-gradient(rgba(255, 255, 255, 0.91), rgba(255, 255, 255, 0.91)),
linear-gradient(90deg, #f2eee8 0%, #fbfaf8 40%, #f1ede7 100%);
}
.field-frame() {
width: 100%;
min-height: 4rem;
border: 0.1rem solid #979797;
}
.title {
font-family: 'KaiseiOpti-Bold';
font-size: 4rem;
line-height: 3.6rem;
color: #232323;
}
.control-wrapper() {
box-shadow: none;
border-radius: 0;
padding: 0 2rem;
}
.slogan {
font-family: 'KaiseiOpti-Regular';
font-size: 1.6rem;
line-height: 2.4rem;
color: #585858;
}
.banner {
height: 14.8rem;
row-gap: 1.2rem;
background: linear-gradient(rgba(255, 255, 255, 0.91), rgba(255, 255, 255, 0.91)),
linear-gradient(90deg, #f2eee8 0%, #fbfaf8 40%, #f1ede7 100%);
.setting-content {
padding: 4rem 18rem 7rem;
}
.title {
font-family: 'KaiseiOpti-Bold';
font-size: 4rem;
line-height: 3.6rem;
color: #232323;
}
.gap {
height: 0.05rem;
margin-top: 6rem;
margin-bottom: 4rem;
background-color: #c4c4c4;
.slogan {
font-family: 'KaiseiOpti-Regular';
font-size: 1.6rem;
line-height: 2.4rem;
color: #585858;
}
}
.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;
}
.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;
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;
}
}
.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;
}
.verified-tip {
color: #6f7f68;
}
&.bottom-gap {
margin-top: 4rem;
}
}

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

@@ -1,7 +1,9 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted, reactive, toRefs } from "vue";
import myEvent from '@/utils/myEvent'
import scList from '@/views/shoppingCart/sc-list.vue'
// import scList from '@/views/shoppingCart/sc-list.vue'
import { useRouter } from "vue-router";
import img from '@/assets/images/brand-null.png'
//const props = defineProps({
//})
@@ -9,11 +11,17 @@ import scList from '@/views/shoppingCart/sc-list.vue'
//])
let data = reactive({
})
const isShoppingShow = ref(false)
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
@@ -27,14 +35,182 @@ defineExpose({})
const {} = toRefs(data);
</script>
<template>
<el-drawer v-model="isShoppingShow" width="50rem" :close-on-click-modal="false" title="I am the title" :with-header="false">
<sc-list is-mini style="flex: 0.6;" @close="shoppingClose"/>
<el-drawer v-model="isShoppingShow" width="50rem" class="addShoppingDrawer" :close-on-click-modal="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>
.homeNavBox{
.addShoppingInfo{
width: 100%;
height: 100%;
position: relative;
display: flex;
flex-direction: column;
> .header{
border-bottom: 0.1rem solid #c4c4c4;
display: flex;
padding-bottom: 2.4rem;
align-items: center;
justify-content: space-between;
> .title{
font-family: KaiseiOpti-Bold;
font-size: 2rem;
line-height: 120%;
color: #121212;
font-weight: 400;
}
> .close{
cursor: pointer;
}
}
> .content{
flex: 1;
padding-top: 9.6rem;
display: flex;
flex-direction: column;
align-items: center;
> .img-list{
height: 37.5rem;
width: 33.7rem;
position: relative;
> .img-box{
position: absolute;
overflow: hidden;
width: 26.9rem;
height: 34.1rem;
border: 1px solid #EFEFEF;
top: 1.7rem;
right: 0;
left: auto;
transform-origin: bottom;
box-shadow: 1rem .8rem 2.4rem 0px #4D4D4D0A;
> img{
width: 100%;
height: 100%;
object-fit: cover;
}
&:nth-child(1){
transform: rotate(-8deg);
background-color: #eaeaea;
right: 2rem;
}
&:nth-child(2){
transform: rotate(-4deg);
background-color: #eeeeee;
right: 1rem;
}
&:nth-child(3){
transform: rotate(0);
background-color: #fafafa;
}
}
}
> .inf-box{
margin-top: 5.18rem;
display: flex;
flex-direction: column;
align-items: center;
> .name{
font-family: KaiseiOpti-Bold;
font-weight: 700;
font-size: 2.4rem;
line-height: 140%;
}
> .shopping-name{
margin-top: 1.3rem;
font-weight: 500;
font-size: 1.6rem;
line-height: 140%;
display: flex;
align-items: center;
> .icon{
margin-right: 0.8rem;
}
}
> .price{
margin-top: 1.2rem;
font-family: KaiseiOpti-Bold;
font-weight: 700;
font-style: Bold;
font-size: 1.8rem;
line-height: 140%;
> .currency{
font-size: 1.2rem;
font-weight: 500;
line-height: 140%;
}
}
}
> .statement{
margin-top: 5rem;
font-family: KaiseiOpti-Regular;
color: #979797;
display: flex;
font-weight: 400;
font-size: 1.2rem;
line-height: 140%;
> .icon{
margin-right: 0.8rem;
}
}
}
> .button{
font-family: KaiseiOpti-Regular;
font-weight: 400;
font-size: 14px;
line-height: 4.6rem;
letter-spacing: 3%;
text-align: center;
width: 100%;
border: 1px solid #232323;
margin-bottom: calc(6rem - 2.4rem);
cursor: pointer;
}
}
</style>

View File

@@ -18,16 +18,6 @@
>
</div>
<br />
<div class="total-file-size">
<span class="label">
<span class="icon"><svg-icon name="order-file" size="18" /></span>
<span class="text">Total File Size</span>
</span>
<span class="value"
>{{ totalSize.size }} <span>{{ totalSize.unit }}</span></span
>
</div>
<div class="hr"></div>
<br />
<div class="total">
<span class="label">Total</span>
@@ -65,14 +55,6 @@
})
return arr
})
const totalSize = computed(() => {
const total = props.list.reduce((pre, cur) => pre + cur.fileSize, 0)
const str = FormatBytes(total)
return {
size: str.split(' ')[0],
unit: str.split(' ')[1]
}
})
const totalAmount = computed(() => props.list.reduce((pre, cur) => pre + cur.amount, 0).toFixed(2))
const handleCheckout = () => {
console.log('购买:', props.list)

View File

@@ -1,200 +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="size" v-if="showSize">
<div class="icon"><svg-icon name="order-file" size="18" /></div>
<div class="text">
<span>{{ FormatBytes(info.fileSize) }}</span>
<span v-if="showSizeDate"
>&nbsp;&nbsp;&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;{{
FormatDate(info.date, 'SM D, YYYY, h:mm A')
}}</span
>
</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>
<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 },
showSize: { type: Boolean, default: true },
showSizeDate: { 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)
}
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;
}
}
> .size {
display: flex;
align-items: center;
> .icon {
width: 2.4rem;
height: 2.4rem;
margin-right: 1rem;
color: #808080;
}
.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);
> .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;
}
> .content {
min-width: 0;
}
> .right {
display: contents;
> .right {
display: contents;
> .amount {
grid-column: 3;
align-self: center;
white-space: nowrap;
transform: translateX(var(--sc-item-order-actions-offset, 0));
}
> .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));
}
}
}
}
.c-svg {
width: initial;
height: initial;
}
> .download {
grid-column: 4;
cursor: pointer;
transform: translateX(var(--sc-item-order-actions-offset, 0));
}
}
}
}
</style>

View File

@@ -1,19 +1,26 @@
<template>
<div class="sc-list-null">
<img src="@/assets/images/shopping-cart-null.png" alt="" />
<div class="title">Your Cart is empty</div>
<div class="tip">Discover new fashion assets and add them to your cart.</div>
<button custom v-show="showButton" @click="handleExploreClick">EXPLORE DIGITAL ITEMS</button>
<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({
showButton: { type: Boolean, default: true }
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 handleExploreClick = () => {
console.log('探索')
const emit = defineEmits(['explore'])
const handleClick = () => {
console.log('emit("explore")')
emit('explore')
}
</script>
@@ -33,6 +40,7 @@
> .title {
font-family: KaiseiOpti-Bold;
font-size: 1.6rem;
line-height: 2.4rem;
color: #979797;
margin-bottom: 0.8rem;
}
@@ -40,6 +48,7 @@
width: 50%;
font-family: KaiseiOpti-Regular;
font-size: 1.4rem;
line-height: 2.4rem;
color: #979797;
}
> button {
@@ -49,7 +58,7 @@
font-family: KaiseiOpti-Medium;
font-size: 1.6rem;
color: #979797;
margin-top: 4.2rem;
margin-top: 3rem;
}
}
</style>

View File

@@ -37,14 +37,18 @@
</div>
</div>
<div class="list">
<sc-list-null v-show="list.length === 0" :show-button="!isMini" />
<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-size="!isMini"
:show-size-date="!isMini"
:show-date="!isMini"
:show-remove="!isView"
@remove="handleRemoveClick"
>
@@ -54,12 +58,6 @@
</sc-item>
</div>
<div class="footer" v-if="isMini">
<div class="total size" v-show="isView">
<span class="label">Total File Size</span>
<span class="value"
>{{ allTotalSize.size }}<span>&nbsp;{{ allTotalSize.unit }}</span></span
>
</div>
<div class="total" v-show="list.length > 0 || isView">
<span class="label">Total</span>
<span class="value">${{ allAmount }}<span> HKD</span></span>
@@ -117,7 +115,6 @@
url: 'http://118.31.39.42:3000/falls/shopping-cart-1.png',
title: 'North Outfit Set',
brand: 'Roaming Clouds',
fileSize: 1024, // kb
date: '2026-5-20 5:20',
amount: 49.99,
tags: ['female', 'skirt', 'blouse', 'outwear'],
@@ -128,7 +125,6 @@
url: 'http://118.31.39.42:3000/falls/shopping-cart-2.png',
title: 'Weekend Drift Co-ord',
brand: 'Urban Line Edit',
fileSize: 1225, // kb
date: '2026-5-21 13:14',
amount: 9.99,
tags: ['female', 'skirt', 'blouse', 'outwear'],
@@ -139,7 +135,6 @@
url: 'http://118.31.39.42:3000/falls/shopping-cart-3.png',
title: 'Static Street Suit',
brand: 'Off Grid Apparel',
fileSize: 1024 * 18, // kb
date: '2026-5-21 13:14',
amount: 12,
tags: ['female', 'skirt', 'blouse', 'outwear'],
@@ -150,7 +145,6 @@
url: 'http://118.31.39.42:3000/falls/shopping-cart-4.png',
title: 'Maison Contour Suit',
brand: 'Ivory Muse Studio',
fileSize: 100, // kb
date: '2026-5-21 13:14',
amount: 18,
tags: ['female', 'skirt', 'blouse', 'outwear'],
@@ -161,7 +155,6 @@
url: 'http://118.31.39.42:3000/falls/shopping-cart-5.png',
title: 'Prime Atelier Set',
brand: 'Ivory Muse Studio',
fileSize: 1024 * 24, // kb
date: '2026-5-21 13:14',
amount: 20,
tags: ['female', 'skirt', 'blouse', 'outwear'],

View File

@@ -94,6 +94,7 @@
:url="item.url"
:name="item.title"
:price="item.price"
:showPrice="false"
></CommodityItem>
</div>
</div>

View File

@@ -65,8 +65,7 @@
class="order-card__item"
:style="{ '--order-item-placeholder': item.color }"
:info="item"
:show-size="false"
:show-size-date="false"
:show-date="false"
:show-remove="false"
order-actions-layout
/>