Merge branch 'main' of ssh://18.167.251.121:10002/aidlab/Aida_Purchaser_Front

This commit is contained in:
2026-04-22 10:31:34 +08:00
87 changed files with 3500 additions and 50 deletions

View File

@@ -0,0 +1,169 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted, reactive, toRefs } from "vue";
import img from "@/assets/images/collectionStory/Rectangle.png";
//const props = defineProps({
//})
const emit = defineEmits([
'addShopping'
])
let data = reactive({
})
const list = ref([
{
url: img,
title: "Windswept Burden",
price: "$100.00",
},{
url: img,
title: "Windswept Burden",
price: "$100.00",
},{
url: img,
title: "Windswept Burden",
price: "$100.00",
},{
url: img,
title: "Windswept Burden",
price: "$100.00",
},{
url: img,
title: "Windswept Burden",
price: "$100.00",
},{
url: img,
title: "Windswept Burden",
price: "$100.00",
},{
url: img,
title: "Windswept Burden",
price: "$100.00",
},{
url: img,
title: "Windswept Burden",
price: "$100.00",
},
])
const type = ref('All')
const addShopping = (item) => {
emit('addShopping', item)
}
onMounted(()=>{
})
onUnmounted(()=>{
})
defineExpose({})
const {} = toRefs(data);
</script>
<template>
<div class="commodityList">
<div class="header">
<div class="title">
Items
</div>
<div class="menu">
<div :class="{'active': type === 'All'}" @click="type = 'All'">All</div>
<div :class="{'active': type === 'Male'}" @click="type = 'Male'">Male</div>
<div :class="{'active': type === 'Female'}" @click="type = 'Female'">Female</div>
</div>
</div>
<div class="list">
<div class="item" v-for="item in list" :key="item.url">
<CommodityItem :url="item.url" :name="item.title" :price="item.price" @addShopping="addShopping(item)"></CommodityItem>
</div>
</div>
</div>
</template>
<style lang="less" scoped>
.commodityList{
width: 100%;
height: 100%;
position: relative;
display: flex;
flex-direction: column;
.header{
position: sticky;
top: 0;
z-index: 2;
background-color: #fff;
.title{
font-family: "KaiseiOpti-Bold";
font-weight: 700;
font-size: 3.6rem;
line-height: 6rem;
color: #121212;
padding: 4rem 0 3.6rem 1.2rem;
}
.menu{
padding: 0 1.2rem;
display: flex;
gap: 2rem;
margin-bottom: 6rem;
> div{
min-width: 6rem;
text-align: center;
font-family: "KaiseiOpti-Bold";
font-weight: 700;
font-size: 1.98rem;
line-height: 100%;
position: relative;
color: #7B7B7B;
cursor: pointer;
&::after{
content: '';
position: absolute;
bottom: -5px;
left: 0;
display: block;
width: 100%;
border-bottom: 1px solid #232323;
display: none;
}
&.active{
color: #232323;
&::after{
display: block;
}
}
}
}
}
.list{
border-top: 0.5px solid #585858;
width: 100%;
flex: 1;
display: grid;
align-content: start;
grid-template-columns: repeat(3, 1fr);
overflow-y: auto;
/* 垂直线(右边框) */
.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;
}
}
}
</style>

72
src/views/brand/index.vue Normal file
View File

@@ -0,0 +1,72 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted, reactive, toRefs } from "vue";
import CommodityList from "./commodity-list.vue";
import MerchantInfo from "./merchant-info.vue";
//const props = defineProps({
//})
//const emit = defineEmits([
//])
let data = reactive({
})
const addShopping = (item) => {}
onMounted(()=>{
})
onUnmounted(()=>{
})
defineExpose({})
const {} = toRefs(data);
</script>
<template>
<div class="brand">
<div class="header-img">
<img src="@/assets/images/brand/brandBg.png" alt="">
</div>
<div class="content">
<div class="merchant-info">
<MerchantInfo></MerchantInfo>
</div>
<div class="commodity-list">
<CommodityList @addShopping="addShopping"></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

@@ -0,0 +1,129 @@
<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="merchantInfo">
<div class="profile">
<img src="@/assets/images/collectionStory/Rectangle.png" alt="">
</div>
<div class="info">
<div class="detail">
<div class="name">Lian Su</div>
<div class="title">Roaming Clouds</div>
</div>
<div class="contact">
<div class="title">Contact</div>
<div class="email label">
<div class="icon">
<svg-icon name="brand-email" size="24" />
</div>
<div>lian.su@urieworweoo.com</div>
</div>
<div class="phone label">
<div class="icon">
<svg-icon name="brand-call" size="24" />
</div>
<div>+86 139 4829 7710</div>
</div>
<div class="address label">
<div class="icon">
<svg-icon name="brand-link" size="24" />
</div>
<div>746312432</div>
</div>
<div class="website label">
<div class="icon">
<svg-icon name="brand-link" size="24" />
</div>
<div>https://urieworweoo.com</div>
</div>
</div>
<div class="about">
<div class="title">About</div>
<div class="content">Lian Sus work weaves understated ethnic influences into contemporary minimalism. She explores materials and silhouettes that bridge heritage and modern sensibilities. Her designs reflect a quiet dialogue between cultural memory and forward-looking innovation.</div>
</div>
</div>
</div>
</template>
<style lang="less" scoped>
.merchantInfo{
width: 100%;
height: auto;
position: relative;
padding-top: 4rem;
padding-bottom: 4rem;
.profile{
width: 20rem;
height: 20rem;
margin-left: 1.8rem;
>img{
width: 100%;
height: 100%;
object-fit: cover;
}
}
.info{
display: flex;
flex-direction: column;
gap: 6rem;
margin-top: 4rem;
.title{
font-family: "KaiseiOpti-Bold";
font-weight: 700;
font-size: 3.4rem;
line-height: 3.6rem;
}
> .detail{
.name{
margin-bottom: .8rem;
font-weight: 500;
font-size: 1.8rem;
line-height: 100%;
color: #232323;
}
}
> .contact{
.title{
margin-bottom: 2rem;
}
.label{
display: flex;
gap: 2rem;
margin-bottom: .6rem;
font-family: "KaiseiOpti-Regular";
font-weight: 400;
font-size: 1.4rem;
line-height: 100%;
color: #585858;
&:last-child{
margin-bottom: 0;
}
}
}
> .about{
.title{
margin-bottom: 2rem;
}
.content{
font-family: "KaiseiOpti-Regular";
font-weight: 400;
font-size: 1.6rem;
line-height: 2.3rem;
color: #585858;
}
}
}
}
</style>

View File

@@ -0,0 +1,142 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted, reactive, toRefs } from "vue";
import { Swiper, SwiperSlide } from 'swiper/vue';
import 'swiper/css';
import 'swiper/css/pagination';
import 'swiper/css/navigation';
import { Navigation, Pagination, Autoplay } from 'swiper/modules'
import coreconcept_1 from "@/assets/images/collectionStory/coreconcept_1.png";
import coreconcept_2 from "@/assets/images/collectionStory/coreconcept_2.png";
import coreconcept_3 from "@/assets/images/collectionStory/coreconcept_3.png";
import coreconcept_4 from "@/assets/images/collectionStory/coreconcept_4.png";
import coreconcept_5 from "@/assets/images/collectionStory/coreconcept_5.png";
//const props = defineProps({
//})
//const emit = defineEmits([
//])
const modules = [Navigation, Pagination, Autoplay]
let data = reactive({
})
let slides = [
{ image: coreconcept_1, text: 'Layered Heritage, Coastal Stillness' },
{ image: coreconcept_2, text: 'Nomadic Silhouettes, Golden Wandering' },
{ image: coreconcept_3, text: 'Pastoral Communion, Woven Earth' },
{ image: coreconcept_4, text: 'Layered winter silhouettes' },
{ image: coreconcept_5, text: 'Insulated winterwear inspired by high-altitude traditions' },
]
onMounted(()=>{
})
onUnmounted(()=>{
})
defineExpose({})
const {} = toRefs(data);
</script>
<template>
<div class="core-concept">
<div class="title">Core Concept</div>
<div class="info">
<div>
We are spiritual nomads carrying what wind cannot take. This collection honors those who knew home is not a place, but what you wear. Earthy palettes of terracotta, ochre, and natural beige evoke pastoral landscapes, while layered textiles and handwoven fabrics embody portable heritage. Each garment balances burden and beautysustainable materials and artisanal craftsmanship create timeless pieces that travel with the wearer, transforming clothing into sanctuary and memory woven into fabric.
</div>
<br />
<div>
This winterwear series reinterprets high-altitude nomadic clothing through modern, refined construction. Layered silhouettes, tactile wools, and structured draping create warmth while preserving fluid movement. Textural contrasts and sculptural volumes evoke the surrounding snowy landscapes and monastic architecture. The collection balances cultural references with contemporary tailoring, presenting outerwear that feels protective, grounded, and quietly elegant within its harsh winter environment.
</div>
</div>
<div class="swiper">
<swiper
:modules="modules"
:slides-per-view="'auto'"
space-between="24"
:navigation="{
prevEl: '.custom-prev',
nextEl: '.custom-next'
}"
>
<div class="custom-prev">
<div style="transform: rotate(180deg);">
<svg-icon name="collectionStory-swiperRight" size="24" />
</div>
</div>
<div class="custom-next">
<div>
<svg-icon name="collectionStory-swiperRight" size="24" />
</div>
</div>
<swiper-slide v-for="item in slides" :key="item.id">
<div class="slide-content">
<img :src="item.image" :alt="item.text" />
<div class="text">{{ item.text }}</div>
</div>
</swiper-slide>
</swiper>
</div>
</div>
</template>
<style lang="less" scoped>
.core-concept{
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;
}
}
> .swiper{
width: 100%;
padding: 0 2.4rem;
padding-top: 1rem;
position: relative;
.custom-prev,.custom-next{
position: absolute;
top: 16rem;
z-index: 2;
cursor: pointer;
color: #fff;
filter: drop-shadow(1px 1px 2px rgba(0, 0, 0, .5));
}
.custom-prev{
left: 0;
}
.custom-next{
left: auto;
right: 0;
}
.slide-content{
width: min-content;
> img{
height: 34.2rem;
width: auto;
}
> .text{
margin-top: 1rem;
font-family: "KaiseiOpti-Regular";
font-weight: 400;
font-size: 1.4rem;
line-height: 2.3rem;
}
}
:deep(.swiper-slide){
width: auto;
}
}
}
</style>

View File

@@ -0,0 +1,85 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted, reactive, toRefs } from "vue";
import feeling_1 from "@/assets/images/collectionStory/feeling_1.png";
import feeling_2 from "@/assets/images/collectionStory/feeling_2.png";
//const props = defineProps({
//})
//const emit = defineEmits([
//])
let data = reactive({
})
let slides = [
{ image: feeling_1, text: 'Web interface in AiDA - the process of apparel edit' },
{ image: feeling_2, text: 'Web interface in AiDA-Sketchboard' },
]
onMounted(()=>{
})
onUnmounted(()=>{
})
defineExpose({})
const {} = toRefs(data);
</script>
<template>
<div class="feeling-with-AiDA">
<div class="title">Feeling with AiDA</div>
<div class="info">
<div>
AiDA significantly enhances the creative latitude for cross-disciplinary creators like myself. Its functionalities, particularly the robust line-art extraction and the ability to edit the front and back garment panels, have been a major source of inspiration. This allows me to rapidly prototype by extracting traditional ethnic wear and conduct detailed style modifications directly on the canvas. As an integrated creation platform, AiDA enables a deeper focus on the design process. For seasoned designers, AI serves as a powerful source of inspiration, and AiDA is an exemplary manifestation of this principle.
</div>
</div>
<div class="img-box">
<div class="img-item" v-for="item in slides" :key="item.text">
<img :src="item.image" alt="">
<div class="text">{{ item.text }}</div>
</div>
</div>
</div>
</template>
<style lang="less" scoped>
.feeling-with-AiDA{
width: 100%;
height: auto;
position: relative;
padding: 6rem 0;
padding-bottom: 18rem;
> .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;
}
}
> .img-box{
width: 100%;
margin-top: 4rem;
display: flex;
gap: 2.3rem;
justify-content: center;
> .img-item{
width: min-content;
> img{
margin-bottom: .9rem;
height: 34.3rem;
}
> .text{
font-family: "KaiseiOpti-Regular";
font-weight: 400;
font-size: 1.4rem;
line-height: 2.3rem;
}
}
}
}
</style>

View File

@@ -0,0 +1,142 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted, reactive, toRefs } from "vue";
import img from "@/assets/images/collectionStory/Rectangle.png";
import coreConcept from "./coreConcept.vue";
import inspiration from "./inspiration.vue";
import feelingWithAiDA from "./feelingWithAiDA.vue";
import CommodityItem from "@/components/CommodityItem.vue";
//const props = defineProps({
//})
//const emit = defineEmits([
//])
let data = reactive({
})
const list = ref([
{
url: img,
title: "Windswept Burden",
price: "$100.00",
},{
url: img,
title: "Windswept Burden",
price: "$100.00",
},{
url: img,
title: "Windswept Burden",
price: "$100.00",
},
])
const addShopping = (item) => {
console.log(item);
}
onMounted(()=>{
})
onUnmounted(()=>{
})
defineExpose({})
const {} = toRefs(data);
</script>
<template>
<div class="detail">
<div class="left">
<div class="personal">
<img :src="img" alt="">
<div class="name">
<span>Lian Su</span>
<div class="icon">
<SvgIcon name="share" size="24" />
</div>
</div>
</div>
</div>
<div class="center">
<coreConcept ></coreConcept>
<div class="line"></div>
<inspiration ></inspiration>
<div class="line"></div>
<feelingWithAiDA ></feelingWithAiDA>
</div>
<div class="right">
<div class="item" v-for="item in list" :key="item.url">
<CommodityItem :url="item.url" :name="item.title" :price="item.price" @addShopping="addShopping(item)"></CommodityItem>
</div>
</div>
</div>
</template>
<style lang="less" scoped>
.detail{
width: 100%;
height: auto;
position: relative;
display: flex;
align-items: flex-start;
> div{
// height: 100%;
}
> .left{
width: 23rem;
padding-top: 6.3rem;
padding-left: 3rem;
position: sticky;
top: 0;
> .personal{
display: flex;
flex-direction: column;
> img{
width: 10rem;
height: 10rem;
object-fit: cover;
margin-bottom: 1.1rem;
}
> .name{
display: flex;
gap: .4rem;
align-items: center;
> span{
font-family: 'KaiseiOpti-Bold';
font-weight: 700;
font-size: 1.8rem;
line-height: 100%;
vertical-align: bottom;
}
}
}
}
> .center{
flex: 1;
border-left: 0.5px solid #585858;
border-right: 0.5px solid #585858;
// overflow-y: auto;
overflow: hidden;
// height: 100%;
height: auto;
.line{
border: 0.5px solid #58585899;
width: 100%;
}
&::-webkit-scrollbar{
display: none;
}
}
> .right{
width: 25.4rem;
padding-top: 6rem;
position: sticky;
top: 0;
height: calc(100vh - var(--header-height));
display: flex;
flex-direction: column;
align-items: center;
gap: 4rem;
overflow-y: auto;
&::-webkit-scrollbar{
display: none;
}
> .item{
margin-bottom: 2.3rem;
width: 20rem;
}
}
}
</style>

View File

@@ -0,0 +1,87 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted, reactive, toRefs } from "vue";
import inspiration_1 from "@/assets/images/collectionStory/inspiration_1.png";
import inspiration_2 from "@/assets/images/collectionStory/inspiration_1.png";
//const props = defineProps({
//})
//const emit = defineEmits([
//])
let data = reactive({
})
let slides = [
{ image: inspiration_1, text: 'Moodboard 1 for this Collection' },
{ image: inspiration_2, text: 'Moodboard 2 for this Collection' },
]
onMounted(()=>{
})
onUnmounted(()=>{
})
defineExpose({})
const {} = toRefs(data);
</script>
<template>
<div class="inspiration">
<div class="title">Inspiration</div>
<div class="info">
<div>
This project is inspired by the enduring traditions and unique material culture of high-altitude nomadic communities, particularly those found across Central Asia and the Himalayan plateau.
</div>
<div>
The moodboard now balances rugged, natural fibers and dramatic, layered silhouettes with vibrant color and refined pattern work. Key visual elements include the strength of traditional nomadic crafts like falconry and the warmth of textured knitwear. The palette expands from earthy browns to striking accents of lime green, teal, and gold, often expressed through intricate geometric ornamental motifs. The silhouettes emphasize functional draping (scarves and outerwear) and structured, high-collar layering, translating ancient heritage into a bold, contemporary aesthetic.
</div>
</div>
<div class="img-box">
<div class="img-item" v-for="item in slides" :key="item.text">
<img :src="item.image" alt="">
<div class="text">{{ item.text }}</div>
</div>
</div>
</div>
</template>
<style lang="less" scoped>
.inspiration{
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;
}
}
> .img-box{
width: 100%;
margin-top: 4rem;
display: flex;
gap: 2.3rem;
justify-content: center;
> .img-item{
width: min-content;
> img{
margin-bottom: .9rem;
height: 34.3rem;
}
> .text{
font-family: "KaiseiOpti-Regular";
font-weight: 400;
font-size: 1.4rem;
line-height: 2.3rem;
}
}
}
}
</style>

View File

@@ -1,11 +1,13 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted, reactive, toRefs } from "vue";
import Detail from "./detail/index.vue";
//const props = defineProps({
//})
//const emit = defineEmits([
//])
let data = reactive({
})
onMounted(()=>{
})
onUnmounted(()=>{
@@ -15,9 +17,39 @@ const {} = toRefs(data);
</script>
<template>
<div class="collectionStory">
<div class="title">
collection Story
<div class="first-screen">
<img class="banner" src="@/assets/images/collectionStory/collection_story_banner.png" alt="">
<div class="back">
<SvgIcon name="collectionStory-back" size="20" />
<div class="text">Back to Home</div>
</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>
<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>
</div>
<div class="scrolling-learn-more">
<div>Scrolling Learn More</div>
<SvgIcon name="collectionStory-scrollingLearnMore" size="48" />
</div>
</div>
</div>
<div class="content">
<Detail></Detail>
</div>
<Footer></Footer>
</div>
</template>
<style lang="less" scoped>
@@ -25,5 +57,104 @@ const {} = toRefs(data);
width: 100%;
height: 100%;
position: relative;
overflow-y: auto;
.first-screen{
position: relative;
height: auto;
display: flex;
overflow: hidden;
> .back{
position: absolute;
top: 2.4rem;
left: 2.4rem;
display: flex;
align-items: center;
gap: .8rem;
color: #fff;
cursor: pointer;
> .text{
font-size: 2rem;
font-weight: 500;
white-space: nowrap;
}
}
> .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%);
display: flex;
flex-direction: column;
color: #fff;
animation: scroll 3s linear infinite;
@keyframes scroll {
0% {
transform: translateY(0);
}
50% {
transform: translateY(-20%);
}
100% {
transform: translateY(0%);
}
}
> 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;
}
}
.content{
width: 100%;
// min-height: 100%;
// height: 100%;
}
}
</style>

View File

@@ -1,18 +1,22 @@
<template>
<div class="home-index">
<section class="index">
<img src="@/assets/images/home-bg.jpg" class="bg" />
</section>
<section class="designers"></section>
<section class="design"></section>
<section class="digital-items"></section>
<section class="digital-items"></section>
<section class="footer"></section>
<section-index />
<section-designers />
<section-design />
<section-digital-items1 />
<section-digital-items2 />
<section-footer />
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import SectionIndex from './section-index.vue'
import SectionDesigners from './section-designers.vue'
import SectionDesign from './section-design.vue'
import SectionDigitalItems1 from './section-digital-items1.vue'
import SectionDigitalItems2 from './section-digital-items2.vue'
import SectionFooter from './section-footer.vue'
</script>
<style lang="less">
@@ -22,14 +26,18 @@
overflow: hidden;
overflow-y: auto;
> section {
position: relative;
width: 100%;
height: auto;
}
> section.index {
> section.bgw {
position: relative;
> .bg {
width: 100%;
height: auto;
display: block;
}
> .content {
position: absolute;
}
}
}

View File

@@ -0,0 +1,49 @@
<template>
<section class="section-design bgw">
<img src="@/assets/images/home/design-bg.jpg" class="bg" />
<div class="content">
<div class="aida-logo"><img src="@/assets/images/logos/aida.png" /></div>
<div class="title">Design with AiDA</div>
<div class="tip">
Each garment on this platform is where designer vision blooms through AiDA. A tool that
nurtures your creativity, never overshadows it. Let your ideas flourish.
</div>
<button custom>Try Now</button>
</div>
</section>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
</script>
<style lang="less">
.section-design {
> .content {
width: 55rem;
top: 12rem;
left: 9rem;
> .aida-logo {
margin-bottom: 6rem;
> img {
width: auto;
height: 6.9rem;
}
}
> .title {
font-family: KaiseiOpti-Bold;
font-size: 5.6rem;
line-height: 6.2rem;
margin-bottom: 2rem;
color: #fff;
}
> .tip {
font-family: KaiseiOpti-Regular;
font-size: 2rem;
line-height: 2.8rem;
color: #fff;
margin-bottom: 10rem;
}
}
}
</style>

View File

@@ -0,0 +1,130 @@
<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

@@ -0,0 +1,101 @@
<template>
<section class="digital-items1 bgw">
<img src="@/assets/images/home/digital-items-1.jpg" class="bg" />
<div class="content">
<div class="title">Digital Items</div>
<div class="tip">
AiDA captures your boldest thoughts and transforms them into vivid
<br />
digital visionsa virtual realm where creativity collides and evolves.
</div>
<button custom="black">Shop All</button>
<div class="list">
<div v-for="v in list" :key="v.url">
<img :src="v.url" alt="" />
<div class="title">{{ v.title }}</div>
<div class="tip">{{ v.tip }}</div>
</div>
</div>
</div>
</section>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
const list = ref([
{
title: 'Womens Itemk',
tip: 'Blue Pleat Aria',
url: 'http://118.31.39.42:3000/falls/digital-items-1.png'
},
{
title: 'Girls Item',
tip: 'Candy Riot',
url: 'http://118.31.39.42:3000/falls/digital-items-2.jpg'
},
{
title: 'Mens Item',
tip: 'Void Armour',
url: 'http://118.31.39.42:3000/falls/digital-items-3.png'
},
{
title: 'Boys Item',
tip: 'Jester Edit',
url: 'http://118.31.39.42:3000/falls/digital-items-4.png'
}
])
</script>
<style lang="less">
.digital-items1 {
> .content {
top: 8rem;
left: 0;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
> .title {
font-family: KaiseiOpti-Bold;
font-size: 5.6rem;
line-height: 6.2rem;
margin-bottom: 2rem;
}
> .tip {
font-family: KaiseiOpti-Regular;
font-size: 2rem;
line-height: 2.8rem;
margin-bottom: 2rem;
color: #585858;
text-align: center;
}
> .list {
margin-top: 6rem;
display: flex;
gap: 2.4rem;
> div {
padding: 1rem;
border: 0.1rem solid #979797;
> img {
width: 27.4rem;
height: 34.6rem;
margin-bottom: 1.2rem;
}
> .title {
font-family: KaiseiOpti-Regular;
font-size: 1.8rem;
line-height: 2.2rem;
color: #232323;
margin-bottom: 0.6rem;
}
> .tip {
font-family: KaiseiOpti-Regular;
font-size: 1.2rem;
line-height: 2.2rem;
color: #979797;
}
}
}
}
}
</style>

View File

@@ -0,0 +1,94 @@
<template>
<section class="digital-items2 bgw">
<img src="@/assets/images/home/digital-items-1.jpg" class="bg" />
<div class="content">
<div class="tip">
AiDA accelerates style innovation, shaping daily pieces that keep
<br />
your wardrobe in sync with modern fashions rhythm.
</div>
<div class="list">
<div v-for="v in list" :key="v.url">
<img :src="v.url" alt="" />
<div class="title">{{ v.title }}</div>
<div class="tip">{{ v.tip }}</div>
</div>
</div>
</div>
</section>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
const list = ref([
{
title: 'Womens Itemk',
tip: 'Blue Pleat Aria',
url: 'http://118.31.39.42:3000/falls/digital-items-1.png'
},
{
title: 'Girls Item',
tip: 'Candy Riot',
url: 'http://118.31.39.42:3000/falls/digital-items-2.jpg'
},
{
title: 'Mens Item',
tip: 'Void Armour',
url: 'http://118.31.39.42:3000/falls/digital-items-3.png'
},
{
title: 'Boys Item',
tip: 'Jester Edit',
url: 'http://118.31.39.42:3000/falls/digital-items-4.png'
}
])
</script>
<style lang="less">
.digital-items2 {
> .content {
top: 8rem;
left: 0;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
> .tip {
font-family: KaiseiOpti-Regular;
font-size: 2rem;
line-height: 2.8rem;
margin-bottom: 2rem;
color: #585858;
text-align: center;
}
> .list {
margin-top: 6rem;
display: flex;
gap: 2.4rem;
> div {
padding: 1rem;
border: 0.1rem solid #979797;
> img {
width: 27.4rem;
height: 34.6rem;
margin-bottom: 1.2rem;
}
> .title {
font-family: KaiseiOpti-Regular;
font-size: 1.8rem;
line-height: 2.2rem;
color: #232323;
margin-bottom: 0.6rem;
}
> .tip {
font-family: KaiseiOpti-Regular;
font-size: 1.2rem;
line-height: 2.2rem;
color: #979797;
}
}
}
}
}
</style>

View File

@@ -0,0 +1,157 @@
<template>
<section class="section-footer">
<div class="content">
<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/aida-black.png" />
</div>
<div class="tip">
Stylish Parade is a commerce platform for designers, serving as AiDA's commercial
extension.
</div>
<div class="link">
<span class="text">Bloom your Creativity with AiDA!</span>
<span class="icon"><svg-icon name="arrow_right" size="12" /></span>
</div>
</div>
<div class="help">
<div class="title">HELP</div>
<div class="item">FAQ</div>
<div class="item">My Account</div>
<div class="item">My Orders</div>
<div class="item">Payment and Invoices</div>
<div class="item">Copyright Licence</div>
</div>
<div class="polices">
<div class="title">POLICES</div>
<div class="item">Legal</div>
<div class="item">Privacy Policy</div>
<div class="item">Cookies Settings</div>
<div class="item">Purchase Conditions</div>
</div>
<div class="company">
<div class="title">COMPANY</div>
<div class="item">About us</div>
<div class="item">Offices</div>
<div class="item">Join with us</div>
</div>
</div>
<div class="footer">
<div class="left">© Code-Create 2026</div>
<div class="right">
<img src="@/assets/images/icons/xiaohongshu.png" />
<img src="@/assets/images/icons/linkedin.png" />
<img src="@/assets/images/icons/instagram.png" />
<img src="@/assets/images/icons/facebook.png" />
<img src="@/assets/images/icons/douyin.png" />
<img src="@/assets/images/icons/wechat.png" />
</div>
</div>
</section>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
</script>
<style lang="less">
.section-footer {
background-color: #f6f6f6;
border-top: 0.1rem solid #232323;
> .content {
display: flex;
min-height: 37rem;
padding: 0 8rem;
border-bottom: 0.1rem solid #c4c4c4;
> div {
display: flex;
flex-direction: column;
align-items: flex-start;
padding: 3rem 2rem;
flex: 1;
border-right: 0.1rem solid #c4c4c4;
&:last-child {
border-right: none;
}
}
> .mate {
> .logos {
margin-bottom: 3rem;
display: flex;
gap: 2.9rem;
> img {
width: auto;
height: 3rem;
}
}
> .tip {
font-family: KaiseiOpti-Regular;
font-size: 1.4rem;
line-height: 140%;
color: #585858;
margin-bottom: 4.4rem;
}
> .link {
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 0.1rem solid #232323;
user-select: none;
cursor: pointer;
> .text {
font-size: 1.4rem;
line-height: 120%;
color: #232323;
margin-right: 1rem;
}
> .icon {
width: 2.4rem;
height: 2.4rem;
transform: rotate(-45deg);
}
}
}
> .help,
> .polices,
> .company {
> .title {
font-family: KaiseiOpti-Bold;
font-size: 1.8rem;
color: #232323;
margin-bottom: 2.3rem;
}
> .item {
font-family: KaiseiOpti-Regular;
font-size: 1.4rem;
color: #585858;
margin-bottom: 0.6rem;
line-height: 2rem;
user-select: none;
cursor: pointer;
}
}
}
> .footer {
padding: 0 8rem;
height: 7rem;
display: flex;
align-items: center;
justify-content: space-between;
> .left {
font-family: KaiseiOpti-Regular;
font-size: 1.2rem;
color: #585858;
}
> .right {
display: flex;
> img {
width: 2rem;
height: 2rem;
margin-left: 1.6rem;
}
}
}
}
</style>

View File

@@ -0,0 +1,75 @@
<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>
<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>
</section>
</template>
<script setup lang="ts">
import { computed } from 'vue'
</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;
left: 9rem;
color: #fff;
> .title {
font-family: KaiseiOpti-Bold;
font-size: 6rem;
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;
}
}
> p.tip {
font-family: KaiseiOpti-Regular;
font-size: 1.2rem;
line-height: 2rem;
color: #ededed;
}
}
}
</style>

View File

@@ -0,0 +1,130 @@
.retrieve-password,
.register,
.login {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
margin-top: 2rem;
}
.retrieve-password:deep(.el-form),
.register:deep(.el-form),
.login:deep(.el-form) {
width: 100%;
}
.retrieve-password:deep(.el-form) .el-form-item,
.register:deep(.el-form) .el-form-item,
.login:deep(.el-form) .el-form-item {
position: relative;
margin-bottom: 1.6rem;
}
.retrieve-password:deep(.el-form) .el-form-item__content,
.register:deep(.el-form) .el-form-item__content,
.login:deep(.el-form) .el-form-item__content {
position: relative;
}
.retrieve-password:deep(.el-form) .el-form-item__label,
.register:deep(.el-form) .el-form-item__label,
.login:deep(.el-form) .el-form-item__label {
color: #232323;
font-size: 1.2rem;
margin-bottom: 0.8rem;
line-height: 1.5rem;
}
.retrieve-password:deep(.el-form) .el-input,
.register:deep(.el-form) .el-input,
.login:deep(.el-form) .el-input {
--el-input-height: 3.4rem;
--el-input-border-radius: 0;
--el-input-text-color: #232323;
--el-border-color: #C4C4C4;
font-size: 1.4rem;
}
.retrieve-password:deep(.el-form) .el-input::placeholder,
.register:deep(.el-form) .el-input::placeholder,
.login:deep(.el-form) .el-input::placeholder {
color: #9F9F9F;
}
.retrieve-password:deep(.el-form) .password-tip,
.register:deep(.el-form) .password-tip,
.login:deep(.el-form) .password-tip {
position: absolute;
z-index: 10;
top: -1rem;
right: 0;
transform: translateY(-100%);
}
.retrieve-password:deep(.el-form) .password-warning,
.register:deep(.el-form) .password-warning,
.login:deep(.el-form) .password-warning {
--el-checkbox-height: auto;
margin-top: -0.6rem;
margin-bottom: 1.6rem;
display: flex;
align-items: center;
}
.retrieve-password:deep(.el-form) .password-warning > .icon,
.register:deep(.el-form) .password-warning > .icon,
.login:deep(.el-form) .password-warning > .icon {
width: 1.4rem;
height: 1.4rem;
margin-right: 0.8rem;
}
.retrieve-password:deep(.el-form) .password-warning > .label,
.register:deep(.el-form) .password-warning > .label,
.login:deep(.el-form) .password-warning > .label {
font-family: KaiseiOpti-Regular;
font-size: 1rem;
color: #9F9F9F;
}
.retrieve-password:deep(.el-form) .forgotPassword,
.register:deep(.el-form) .forgotPassword,
.login:deep(.el-form) .forgotPassword {
margin-top: -0.8rem;
margin-bottom: 5rem;
font-size: 1.1rem;
text-align: right;
color: #666666;
cursor: pointer;
text-decoration: underline;
font-family: KaiseiOpti-Regular;
}
.retrieve-password:deep(.el-form) .el-form-item__error,
.register:deep(.el-form) .el-form-item__error,
.login:deep(.el-form) .el-form-item__error {
padding-top: 1px;
font-size: 1rem;
}
.retrieve-password:deep(.el-form) .submit,
.register:deep(.el-form) .submit,
.login:deep(.el-form) .submit {
width: 100%;
height: 4rem;
font-size: 1.36rem;
}
.retrieve-password:deep(.el-form) .privacy,
.register:deep(.el-form) .privacy,
.login:deep(.el-form) .privacy {
margin-top: -0.6rem;
--el-checkbox-height: auto;
}
.retrieve-password:deep(.el-form) .privacy .el-checkbox__label,
.register:deep(.el-form) .privacy .el-checkbox__label,
.login:deep(.el-form) .privacy .el-checkbox__label {
font-size: 1.1rem;
color: #666666;
font-family: KaiseiOpti-Regular;
}
.retrieve-password:deep(.el-form) .privacy .el-checkbox__label > div > span,
.register:deep(.el-form) .privacy .el-checkbox__label > div > span,
.login:deep(.el-form) .privacy .el-checkbox__label > div > span {
font-family: KaiseiOpti-Bold;
text-decoration: underline;
cursor: pointer;
color: #232323;
}
.retrieve-password > .other-login,
.register > .other-login,
.login > .other-login {
margin-top: 4rem;
}

View File

@@ -0,0 +1,143 @@
<template>
<div class="visible-code">
<div class="tip" v-html="$t('Login.verifyCodeHasSent', { email: props.email })"></div>
<input-code @submit="onVerify" v-model="code" ref="inputCodeRef" />
<p class="time" v-if="time > 0">{{ $t('Login.resendCodeIn', { time: timeStr }) }}</p>
<p class="time" v-if="time === 0">
<span @click="onResend">{{ $t('Login.resendCode') }}</span>
</p>
<button class="verify" custom="black" @click="onVerify">{{ $t('Login.verify') }}</button>
<other-login />
</div>
</template>
<script setup lang="ts">
import md5 from 'md5'
import { ElMessage } from 'element-plus'
import OtherLogin from './other-login.vue'
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import { CountDown } from '@/utils/tools'
import InputCode from '@/components/input-code.vue'
import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const router = useRouter()
const emit = defineEmits(['submit-email-code'])
const props = defineProps({
email: { type: String, required: true },
type: {
type: String as () => 'LOGIN' | 'REGISTER' | 'FORGOT_PWD',
required: true
},
password: { type: String, default: '' }
})
const code = ref('')
const time = ref(60)
const timeStr = computed(() => CountDown(time.value))
const timeout = ref(null)
const setTime = (s = 120) => {
clearTime()
time.value = s
timeout.value = setInterval(() => {
time.value--
if (time.value <= 0) {
clearTime()
time.value = 0
}
}, 1000)
}
const clearTime = () => {
time.value = -1
clearTimeout(timeout.value)
}
onBeforeUnmount(() => {
clearTime()
})
onMounted(() => {
onSendCode()
})
const inputCodeRef = ref(null)
const resetCode = () => {
inputCodeRef.value?.resetCode?.()
}
const onSendCode = async () => {
resetCode()
const email = props.email
if (!email) {
console.warn('请输入邮箱')
return Promise.reject('请输入邮箱')
}
// const data = {
// email,
// type: props.type
// }
// if (props.type === 'LOGIN') {
// data['password'] = md5(props.password)
// }
// const res = await SendVerificationCode(data)
// if (!res) {
// ElMessage.error(t('Login.sendCodeError'))
// return Promise.reject('发送验证码失败')
// }
setTime()
return Promise.resolve()
}
const onResend = () => {
if (time.value > 0) return
onSendCode()
}
const onVerify = () => {
if (code.value.length !== 6) return
emit('submit-email-code', code.value)
}
defineExpose({
onSendCode
})
</script>
<style lang="less" scoped>
.visible-code {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
> .tip {
text-align: center;
margin-top: 4.6rem;
line-height: 2.4rem;
font-family: KaiseiOpti-Regular;
font-size: 1.6rem;
color: #666;
&:deep(span) {
color: #252727;
font-family: KaiseiOpti-Medium;
}
}
> .input-code {
margin-top: 6rem;
}
> .verify {
margin-top: 5rem;
width: 100%;
height: 4rem;
--button-font-size: 1.4rem;
}
> .time {
user-select: none;
margin-top: 2.4rem;
font-size: 1.6rem;
color: #666;
font-family: Regular;
> span {
color: #ff7a50;
text-decoration: underline;
cursor: pointer;
font-weight: 500;
font-family: Medium;
}
}
> .other-login {
margin-top: 6rem;
}
}
</style>

117
src/views/login/index.vue Normal file
View File

@@ -0,0 +1,117 @@
<template>
<div class="index">
<div class="header">
<span class="tip">{{ $t('AlphaVersion') }}</span>
<img src="@/assets/images/logo-1.png" class="logo" />
<p class="split"></p>
<button class="login" @click="onLogin">{{ $t('Login.login') }}</button>
<button class="register" @click="onRegister">{{ $t('Login.register') }}</button>
</div>
<img src="@/assets/images/login/index-title.png" class="title" draggable="false" />
<img src="@/assets/images/login/index-zhuangshi.png" class="zhuangshi" draggable="false" />
<div class="tip">{{ $t('Login.indexTip') }}</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
const onLogin = () => {
router.push({ name: 'login' })
}
const onRegister = () => {
router.push({ name: 'register' })
}
</script>
<style lang="less" scoped>
.index {
width: 100%;
height: 100%;
overflow: hidden;
position: relative;
background-image: url('@/assets/images/login/index-bg.png');
background-size: cover;
background-position: center;
> .header {
position: absolute;
top: 3rem;
left: 0;
width: 100%;
display: flex;
justify-content: center;
> * {
position: relative;
z-index: 1;
}
> .tip {
position: absolute;
width: 100%;
top: 0;
left: 0;
font-size: 3rem;
text-align: center;
font-family: Regular;
color: #fff;
z-index: 0;
}
> .logo {
width: auto;
height: 2.5rem;
margin-left: 3.8rem;
}
> .split {
margin: 0 auto;
}
> button {
margin-right: 3rem;
width: 20rem;
height: 5.2rem;
border-radius: 5rem;
border: none;
outline: none;
font-size: 2.2rem;
font-weight: 600;
font-family: SemiBold;
border: 0.2rem solid #fff;
&:active {
opacity: 0.8;
}
}
> .login {
background-color: #fff;
color: #713e1f;
}
> .register {
background-color: transparent;
color: #ffffff;
backdrop-filter: blur(10px);
}
}
> .zhuangshi,
> .title,
> .tip {
position: absolute;
left: 50%;
transform: translateX(-50%);
}
> .title {
// width: 55%;
width: 105.6rem;
height: auto;
top: 20.5rem;
}
> .zhuangshi {
width: 21.5rem;
height: auto;
bottom: 13.4rem;
}
> .tip {
font-size: 2.8rem;
font-family: Regular;
color: #fff;
bottom: 8rem;
}
}
</style>

View File

@@ -0,0 +1,120 @@
.retrieve-password,
.register,
.login {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
margin-top: 2rem;
&:deep(.el-form) {
width: 100%;
.el-form-item {
position: relative;
margin-bottom: 1.6rem;
}
.el-form-item__content {
position: relative;
}
.el-form-item__label {
color: #232323;
font-size: 1.2rem;
margin-bottom: 0.8rem;
line-height: 1.5rem;
}
.el-input {
--el-input-height: 3.4rem;
--el-input-border-radius: 0;
--el-input-text-color: #232323;
--el-border-color: #C4C4C4;
font-size: 1.4rem;
&::placeholder {
color: #9F9F9F;
}
}
.password-tip {
position: absolute;
z-index: 10;
top: -1rem;
right: 0;
transform: translateY(-100%);
}
.password-warning {
--el-checkbox-height: auto;
margin-top: -0.6rem;
margin-bottom: 1.6rem;
display: flex;
align-items: center;
>.icon {
width: 1.4rem;
height: 1.4rem;
margin-right: 0.8rem;
}
>.label {
font-family: KaiseiOpti-Regular;
font-size: 1rem;
color: #9F9F9F;
}
}
.forgotPassword {
margin-top: -0.8rem;
margin-bottom: 5rem;
font-size: 1.1rem;
text-align: right;
color: #666666;
cursor: pointer;
text-decoration: underline;
font-family: KaiseiOpti-Regular;
}
.el-form-item__error {
padding-top: 1px;
font-size: 1rem;
}
.submit {
width: 100%;
height: 4rem;
font-size: 1.36rem;
}
.privacy {
margin-top: -0.6rem;
--el-checkbox-height: auto;
.el-checkbox__label {
font-size: 1.1rem;
color: #666666;
font-family: KaiseiOpti-Regular;
>div {
>span {
font-family: KaiseiOpti-Bold;
text-decoration: underline;
cursor: pointer;
color: #232323;
}
}
}
}
}
>.other-login {
margin-top: 4rem;
}
}

View File

@@ -0,0 +1,231 @@
<template>
<el-dialog
class="login-dialog"
v-model="show"
header-class="login-dialog-header"
align-center
destroy-on-close
:show-close="false"
:close-on-press-escape="false"
:close-on-click-modal="false"
>
<div class="login-dialog-content">
<img class="bg" src="@/assets/images/login/bg.jpg" />
<img class="logo" src="@/assets/images/logo.png" />
<div class="close" @click="show = false"><svg-icon name="close" /></div>
<div class="content" v-if="curentTabInfo">
<div class="header">
<div class="title" v-show="curentTabInfo.title">
<div class="icon" @click="onBack"><svg-icon name="back" size="17" /></div>
<div class="label">{{ curentTabInfo.title }}</div>
</div>
<div class="nav" v-show="!curentTabInfo.title">
<div
class="item"
:class="{ active: currentTab === TabNames.sign_up }"
@click="currentTab = TabNames.sign_up"
>
SIGN UP
</div>
<div
class="item"
:class="{ active: currentTab === TabNames.login }"
@click="currentTab = TabNames.login"
>
LOG IN
</div>
</div>
</div>
<component
:is="curentTabInfo.component"
@retrieve-password="currentTab = TabNames.retrieve_password"
@login="onLogin"
@register="onRegister"
@submit-email-code="onSubmitEmailCode"
:name="data.name"
:email="data.email"
:password="data.password"
type="FORGOT_PWD"
/>
</div>
</div>
</el-dialog>
</template>
<script setup lang="ts">
import { computed, ref, markRaw, watch, onBeforeUnmount } from 'vue'
import md5 from 'md5'
import login from './login.vue'
import register from './register.vue'
import emailVerify from './email-verify.vue'
import myEvent from '@/utils/myEvent'
const data = ref({
name: '',
email: '',
password: '',
type: ''
})
const show = ref(false)
const TabNames = {
login: 'login',
sign_up: 'sign_up',
email_verify: 'email_verify',
retrieve_password: 'retrieve_password'
}
const tabList = markRaw([
{
name: TabNames.login,
component: login
},
{
name: TabNames.sign_up,
component: register
},
{
name: TabNames.email_verify,
title: 'EMAIL VERIFICATION',
component: emailVerify
},
{
name: TabNames.retrieve_password,
title: 'RETRIEVE PASSWORD',
component: login
}
])
const currentTab = ref(TabNames.login)
const curentTabInfo = computed(() => tabList.find((v) => v.name === currentTab.value))
const lastTab = ref('')
watch(currentTab, (v, o) => (lastTab.value = o))
const onBack = () => {
if (lastTab.value) currentTab.value = lastTab.value
}
const open = (type?: string) => {
currentTab.value = TabNames[type] || TabNames.login
data.value.name = ''
data.value.email = ''
data.value.password = ''
show.value = true
}
myEvent.add('openLoginDialog', open)
onBeforeUnmount(() => {
myEvent.remove('openLoginDialog', open)
})
const onLogin = (res: any) => {
data.value = res
data.value.type = TabNames.login
currentTab.value = TabNames.email_verify
}
const onRegister = (res: any) => {
data.value = res
data.value.type = TabNames.sign_up
currentTab.value = TabNames.email_verify
}
const onSubmitEmailCode = (code: string) => {
// data.value.code = code
console.log(code)
show.value = false
}
</script>
<style lang="less">
.login-dialog-header {
display: none;
}
.el-dialog.login-dialog {
--el-dialog-border-radius: 0;
--el-dialog-padding-primary: 0;
width: 90rem;
height: 62rem;
.el-dialog__body {
width: 100%;
height: 100%;
}
}
.login-dialog-content {
width: 100%;
height: 100%;
overflow: hidden;
position: relative;
> .bg {
width: 100%;
height: 100%;
display: block;
}
> * {
position: absolute;
}
> .logo {
width: auto;
height: 4rem;
top: 4rem;
left: 3rem;
}
> .close {
width: 3rem;
height: 3rem;
top: 2rem;
right: 2rem;
cursor: pointer;
}
> .content {
width: 34rem;
top: 5rem;
right: 6rem;
height: calc(100% - 10rem);
> .header {
--padding-bottom: 1.2rem;
padding-bottom: var(--padding-bottom);
border-bottom: 0.1rem solid #9f9f9f;
> div {
display: flex;
height: 3rem;
}
> .nav {
> .item {
user-select: none;
cursor: pointer;
flex: 1;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
position: relative;
font-size: 2rem;
font-family: KaiseiOpti-Regular;
color: #9f9f9f;
&.active {
font-family: KaiseiOpti-Bold;
color: #232323;
&::after {
content: '';
position: absolute;
bottom: calc(0rem - var(--padding-bottom) - 0.2rem);
left: 0;
width: 100%;
height: 0;
border-bottom: 0.4rem solid #232323;
}
}
}
}
> .title {
align-items: center;
justify-content: center;
> .icon {
width: 2.4rem;
height: 2.4rem;
cursor: pointer;
margin-right: 1rem;
}
> .label {
font-family: KaiseiOpti-Bold;
font-size: 2rem;
color: #232323;
}
}
}
}
}
</style>

75
src/views/login/login.vue Normal file
View File

@@ -0,0 +1,75 @@
<template>
<div class="login">
<el-form :model="formData" :rules="ruleForm" label-position="top" ref="formRef">
<el-form-item :label="$t('Login.email')" prop="email">
<el-input v-model="formData.email" :placeholder="$t('Login.enterEmail')" name="email" />
</el-form-item>
<el-form-item :label="$t('Login.password')" prop="password">
<el-input
v-model="formData.password"
:placeholder="$t('Login.enterPassword')"
type="password"
show-password
name="password"
/>
</el-form-item>
<div class="forgotPassword">
<span @click="onForgotPassword">{{ $t('Login.forgotPassword') }}</span>
</div>
<el-form-item>
<button type="submit" class="submit" custom="black" @click.prevent="onSubmit">
{{ $t('Login.login') }}
</button>
</el-form-item>
<el-form-item prop="privacy" class="privacy">
<el-checkbox v-model="formData.privacy">
<div v-html="$t('Login.agreeTermsPolicy')"></div>
</el-checkbox>
</el-form-item>
</el-form>
<other-login />
</div>
</template>
<script setup lang="ts">
import { computed, reactive, ref } from 'vue'
import { validateEmail, validatePassLength, validatePrivacy } from './tools'
import OtherLogin from './other-login.vue'
const emit = defineEmits(['retrieve-password', 'login'])
const props = defineProps({
name: { type: String, default: '' },
email: { type: String, default: '' },
password: { type: String, default: '' }
})
const ruleForm = reactive({
email: [{ validator: validateEmail, trigger: 'change' }],
password: [{ validator: validatePassLength, trigger: 'change' }],
privacy: [{ validator: validatePrivacy, trigger: 'change' }]
})
const formData = reactive({
email: props.email,
password: props.password,
privacy: false
})
const formRef = ref(null)
const onForgotPassword = () => {
emit('retrieve-password')
}
const onSubmit = () => {
formRef.value?.validate?.((valid) => {
if (valid) {
emit('login', {
email: formData.email,
password: formData.password
})
} else {
console.warn('error submit!')
}
})
}
</script>
<style lang="less" scoped>
@import './less/style.less';
</style>

View File

@@ -0,0 +1,83 @@
<template>
<div class="other-login">
<div class="title">{{ $t('Login.orContinueWith') }}</div>
<div class="btns">
<button class="submit" custom @click="onGoogle">
<img src="@/assets/images/login/google.png" />
{{ $t('Login.googleLogin') }}
</button>
<button class="submit" custom @click="onWechat">
<img src="@/assets/images/login/wechat.png" />
{{ $t('Login.wechatLogin') }}
</button>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, reactive, ref } from 'vue'
const onGoogle = () => {}
const onWechat = () => {}
</script>
<style lang="less" scoped>
.other-login {
width: 100%;
> .title {
width: 100%;
color: #232323;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.1rem;
font-family: KaiseiOpti-Regular;
line-height: 2rem;
&::before,
&::after {
content: '';
flex: 1;
border-bottom: 0.05rem solid #232323;
}
&::before {
margin-right: 2rem;
}
&::after {
margin-left: 2rem;
}
}
> .btns {
margin-top: 2.4rem;
display: flex;
align-items: center;
justify-content: center;
gap: 2rem;
width: 100%;
overflow: hidden;
> button {
min-width: 0;
flex: 1;
height: 3.4rem;
display: flex;
align-items: center;
justify-content: space-around;
--button-bgcolor: transparent;
--button-color: #666666;
--button-font-size: 1rem;
border: 0.1rem solid #c4c4c4;
&:first-child {
margin-left: 0;
}
img {
width: auto;
height: 2rem;
// margin-right: 2.5rem;
}
}
}
}
</style>

View File

@@ -0,0 +1,60 @@
<template>
<div class="password-tip">
<div>
<el-icon>
<CloseBold v-if="validateLength(value)" />
<Select v-else />
</el-icon>
<span>{{ $t('Login.passwordLengthError', { min: 6, max: 20 }) }}</span>
</div>
<div>
<el-icon>
<CloseBold v-if="validateSpecial(value)" />
<Select v-else />
</el-icon>
<span>{{ $t('Login.passwordSpecial') }}</span>
</div>
<div>
<el-icon>
<CloseBold v-if="validateCase(value)" />
<Select v-else />
</el-icon>
<span>{{ $t('Login.passwordCase') }}</span>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, reactive, ref } from 'vue'
import { Select, CloseBold } from '@element-plus/icons-vue'
import { validateLength, validateSpecial, validateCase } from './tools'
const props = defineProps({
value: {
type: String,
default: ''
}
})
</script>
<style lang="less" scoped>
.password-tip {
background: #404040;
color: #fff;
font-size: 1.4rem;
padding: 2rem;
border-radius: 2rem;
line-height: normal;
> div {
display: flex;
align-items: center;
margin-bottom: 0.5rem;
&:last-child {
margin-bottom: 0;
}
> .el-icon {
margin-right: 1rem;
}
}
}
</style>

View File

@@ -0,0 +1,85 @@
<template>
<div class="register">
<el-form :model="formData" :rules="ruleForm" label-position="top" ref="formRef">
<el-form-item :label="$t('Login.name')" prop="name">
<el-input name="name" v-model="formData.name" :placeholder="$t('Login.enterName')" />
</el-form-item>
<el-form-item :label="$t('Login.password')" prop="password">
<password-tip :value="formData.password" v-show="showPasswordTip" />
<el-input
name="password"
v-model="formData.password"
:placeholder="$t('Login.enterPassword')"
type="password"
show-password
@blur="showPasswordTip = false"
@focus="showPasswordTip = true"
/>
</el-form-item>
<div class="password-warning">
<span class="icon"><svg-icon name="warning" size="12" /></span>
<span class="label">You must satisfy ALL password conditions to register.</span>
</div>
<el-form-item :label="$t('Login.email')" prop="email">
<el-input name="email" v-model="formData.email" :placeholder="$t('Login.enterEmail')" />
</el-form-item>
<el-form-item>
<button type="submit" class="submit" custom="black" @click.prevent="onSubmit">
{{ $t('Login.register') }}
</button>
</el-form-item>
<el-form-item prop="privacy" class="privacy">
<el-checkbox v-model="formData.privacy">
<div v-html="$t('Login.agreeTermsPolicy')"></div>
</el-checkbox>
</el-form-item>
</el-form>
<other-login />
</div>
</template>
<script setup lang="ts">
import { computed, reactive, ref } from 'vue'
import { validateName, validateEmail, validatePass, validatePrivacy } from './tools'
import OtherLogin from './other-login.vue'
import PasswordTip from './password-tip.vue'
const emit = defineEmits(['register'])
const props = defineProps({
name: { type: String, default: '' },
email: { type: String, default: '' },
password: { type: String, default: '' }
})
const ruleForm = reactive({
name: [{ validator: validateName, trigger: 'change' }],
email: [{ validator: validateEmail, trigger: 'change' }],
password: [{ validator: validatePass, trigger: 'change' }],
privacy: [{ validator: validatePrivacy, trigger: 'change' }]
})
const showPasswordTip = ref(false)
const formData = reactive({
name: props.name,
email: props.email,
password: props.password,
privacy: false
})
const formRef = ref(null)
const onSubmit = () => {
formRef.value?.validate?.((valid) => {
if (valid) {
// console.log('submit!')
emit('register', {
name: formData.name,
email: formData.email,
password: formData.password
})
} else {
console.warn('error submit!')
}
})
}
</script>
<style lang="less" scoped>
@import './less/style.less';
</style>

View File

@@ -0,0 +1,120 @@
<template>
<div class="retrieve-password">
<div class="left">
<img class="bg" src="@/assets/images/login/left-bg.png" />
<img class="logo" src="@/assets/images/logo-1.png" />
</div>
<div class="right">
<div class="top">
<button class="back" @click="onBack">
<svg-icon name="arrow-left" size="37" />
</button>
</div>
<div class="box">
<img src="@/assets/images/login/elephant.png" />
<template v-if="!isVisible">
<div class="title">{{ $t('Login.retrievePassword') }}</div>
<el-form :model="formData" :rules="ruleForm" label-position="top" ref="formRef">
<el-form-item :label="$t('Login.email')" prop="email">
<el-input
v-model="formData.email"
:placeholder="$t('Login.enterEmail')"
name="email"
/>
</el-form-item>
<el-form-item :label="$t('Login.password')" prop="password">
<password-tip :value="formData.password" v-show="showPasswordTip" />
<el-input
v-model="formData.password"
:placeholder="$t('Login.enterPassword')"
type="password"
show-password
name="password"
@blur="showPasswordTip = false"
@focus="showPasswordTip = true"
/>
</el-form-item>
<br />
<br />
<el-form-item>
<el-button class="submit" type="primary" @click="onSubmit">{{
$t('submit')
}}</el-button>
</el-form-item>
</el-form>
</template>
<!-- <visible-code
v-show="isVisible"
type="FORGOT_PWD"
ref="visibleCodeRef"
:email="formData.email"
@submit="onVerifyCode"
/> -->
<other-login />
</div>
</div>
</div>
</template>
<script setup lang="ts">
import md5 from 'md5'
import { ForgotPassword } from '@/api/user'
import { computed, reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import { validateEmail, validatePass } from './tools'
import OtherLogin from './other-login.vue'
import emailVerify from './email-verify.vue'
import PasswordTip from './password-tip.vue'
import { useUserInfoStore } from '@/stores'
const userInfoStore = useUserInfoStore()
const router = useRouter()
const ruleForm = reactive({
email: [{ validator: validateEmail, trigger: 'change' }],
password: [{ validator: validatePass, trigger: 'change' }]
})
const isVisible = ref(false)
const showPasswordTip = ref(false)
const formData = reactive({
email: '',
password: ''
})
const formRef = ref(null)
const onBack = () => {
if (isVisible.value) {
isVisible.value = false
} else {
router.back()
}
}
const visibleCodeRef = ref(null)
const onSubmit = () => {
formRef.value?.validate?.((valid) => {
if (valid) {
// console.log('submit!')
visibleCodeRef.value?.onSendCode().then(() => {
isVisible.value = true
})
} else {
console.warn('error submit!')
}
})
}
const onVerifyCode = (code: string) => {
// console.log(code)
ForgotPassword({
email: formData.email,
newPassword: md5(formData.password),
verificationCode: code
})
.then((res) => {
if (res) router.push({ name: 'login' })
})
.catch((error) => {
console.warn(error)
})
}
</script>
<style lang="less" scoped>
@import './less/style.less';
</style>

54
src/views/login/tools.js Normal file
View File

@@ -0,0 +1,54 @@
import i18n from '@/lang'
const t = i18n.global.t
export const validateName = (rule, value, callback) => {
var str = ""
if (!value) {
str = t('Login.pleaseInputName')
} else if (value.length < 2 || value.length > 20) {
str = t('Login.nameLengthError', { min: 2, max: 20 })
}
callback(str ? new Error(str) : undefined)
}
export const validateEmail = (rule, value, callback) => {
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}(\.[a-zA-Z]{2,})?$/
var str = ''
if (!value) {
str = t('Login.pleaseInputEmail')
} else if (!emailRegex.test(value)) {
str = t('Login.emailFormatError')
}
callback(str ? new Error(str) : undefined)
}
// 检查长度
export const validateLength = (v, min = 6, max = 20) => (v.length < 6 || v.length > 20);
//检查特殊字符
export const validateSpecial = (v) => (!/[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(v));
//检查大小写字母和数字
export const validateCase = (v) => (!/[a-z]/.test(v) || !/[A-Z]/.test(v) || !/\d/.test(v));
// 检查密码
export const validatePass = (rule, value, callback) => {
var str = ''
if (validateLength(value)) {
str = t('Login.passwordLengthError', { min: 6, max: 20 })
} else if (validateSpecial(value)) {
str = t('Login.passwordSpecial')
} else if (validateCase(value)) {
str = t('Login.passwordCase')
}
callback(str ? new Error(str) : undefined)
}
// 检查密码长度
export const validatePassLength = (rule, value, callback) => {
var str = ''
if (validateLength(value)) {
str = t('Login.passwordLengthError', { min: 6, max: 20 })
}
callback(str ? new Error(str) : undefined)
}
export const validatePrivacy = (rule, value, callback) => {
if (!value) {
callback(new Error(t('Login.pleaseTermsPolicy')))
} else {
callback()
}
}

View File

@@ -24,8 +24,44 @@
>
<svg-icon :name="activePath === v.path ? v.active_icon : v.icon" size="22" />
</div>
<div class="login">Login</div>
<div class="profile"></div>
<div class="login" @click="onLogin">Login</div>
<el-popover
ref="profilePopover"
placement="bottom-end"
trigger="click"
:show-arrow="false"
popper-style="width: 24rem; padding: 0; border-radius: 0; right: 2rem; top: 10rem;"
>
<template #reference><div class="profile"></div></template>
<template #default>
<div class="profile-content">
<div class="info">
<img src="@/assets/images/profile-content-bg.jpg" alt="" />
<div class="content">
<div class="profile"></div>
<div class="name">Hi, Alexandra_chen</div>
</div>
</div>
<div class="nav-item" @click="onMyWardrobe">
<div class="icon"><svg-icon name="my_wardrobe" size="18" /></div>
<div class="label">My Wardrobe</div>
</div>
<div class="nav-item" @click="onNotifications">
<div class="icon"><svg-icon name="notifications" size="14" /></div>
<div class="label">Notifications</div>
</div>
<div class="nav-item" @click="onSettings">
<div class="icon"><svg-icon name="settings" size="16" /></div>
<div class="label">Settings</div>
</div>
<div class="hr"></div>
<div class="nav-item logout" @click="onLogout">
<div class="icon"><svg-icon name="logout" size="20" /></div>
<div class="label">Log off</div>
</div>
</div>
</template>
</el-popover>
</div>
</div>
</template>
@@ -33,6 +69,7 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import myEvent from '@/utils/myEvent'
const router = useRouter()
const route = useRoute()
const activePath = computed(() => route.path)
@@ -70,11 +107,34 @@
if (path === activePath.value) return
router.push(path)
}
const onLogin = () => {
myEvent.emit('openLoginDialog')
}
const profilePopover = ref(null)
const hideProfilePopover = () => {
profilePopover.value?.hide()
}
const onMyWardrobe = () => {
hideProfilePopover()
console.log('my wardrobe')
}
const onNotifications = () => {
hideProfilePopover()
console.log('notifications')
}
const onSettings = () => {
hideProfilePopover()
console.log('settings')
}
const onLogout = () => {
hideProfilePopover()
console.log('logout')
}
</script>
<style lang="less">
#main-header {
height: 8rem;
height: var(--header-height);
display: flex;
align-items: center;
justify-content: space-between;
@@ -145,4 +205,66 @@
}
}
}
.profile-content {
width: 100%;
> .info {
position: relative;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 2rem;
> img {
width: 100%;
height: auto;
}
> .content {
position: absolute;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
gap: 1rem;
> .profile {
width: 5rem;
height: 5rem;
border-radius: 50%;
background: #cfcfcf;
}
> .name {
font-size: 1.4rem;
color: #232323;
}
}
}
> .hr {
margin: 1.2rem 1rem;
border-top: 0.1rem solid #c4c4c4;
}
> .nav-item {
margin-left: 2rem;
margin-bottom: 1.2rem;
display: flex;
align-items: center;
height: 2rem;
user-select: none;
cursor: pointer;
> .icon {
width: 2rem;
height: 2rem;
margin-right: 1rem;
}
> .label {
font-family: KaiseiOpti-Regular;
font-size: 1.2rem;
color: #585858;
}
}
> .logout {
> .label {
font-family: KaiseiOpti-Medium;
font-size: 1.4rem;
color: #232323;
}
}
}
</style>