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

This commit is contained in:
X1627315083@163.com
2026-04-22 16:29:41 +08:00
19 changed files with 1299 additions and 274 deletions

View File

@@ -107,13 +107,42 @@ body,
--mosaic-bg-size: 1rem;
--mosaic-bg-color1: #efefef;
--mosaic-bg-color2: #fff;
background-image: repeating-conic-gradient(var(--mosaic-bg-color1) 0% 25%, var(--mosaic-bg-color2) 0% 50%);
background-image: repeating-conic-gradient(
var(--mosaic-bg-color1) 0% 25%,
var(--mosaic-bg-color2) 0% 50%
);
background-repeat: repeat;
background-position: 50% 50%;
background-size: var(--mosaic-bg-size) var(--mosaic-bg-size);
}
.flex {
display: flex;
}
.flex-center {
justify-content: center;
align-items: center;
}
.flex-1 {
flex: 1;
}
.flex-col {
flex-direction: column;
}
.align-center {
align-items: center;
}
.space-between {
justify-content: space-between;
}
.justify-center {
justify-content: center;
}
.relative {
position: relative;
}
button[custom],
button[custom="white"] {
button[custom='white'] {
min-width: 19.4rem;
height: 5rem;
padding: 0 1rem;
@@ -126,11 +155,11 @@ button[custom="white"] {
cursor: pointer;
}
button[custom]:active,
button[custom="white"]:active {
button[custom='white']:active {
background: var(--button-click-bgcolor, #e4e4e4);
color: var(--button-click-color, #232323);
}
button[custom="black"] {
button[custom='black'] {
--button-bgcolor: #232323;
--button-color: #fff;
--button-click-bgcolor: #333;

View File

@@ -0,0 +1,5 @@
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="15" cy="15" r="14.5" fill="#232323" stroke="white"/>
<path d="M8.18359 19.5693V22.3285H11.8137C12.056 22.3285 12.2879 22.2298 12.4559 22.055L22.7247 11.3755C23.061 11.0257 23.0556 10.4711 22.7125 10.128L20.4083 7.82384C20.0557 7.47124 19.4824 7.47665 19.1365 7.83584L8.43278 18.9513C8.27291 19.1173 8.18359 19.3388 8.18359 19.5693Z" stroke="white" stroke-width="0.89098" stroke-linecap="round"/>
<path d="M17.9883 8.96387L21.5522 12.5278" stroke="white" stroke-width="0.89098" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 641 B

3
src/assets/icons/dui.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg width="20" height="14" viewBox="0 0 20 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19.7269 0.156104C19.5188 -0.0520347 19.1025 -0.0520347 18.8944 0.156104L7.13454 11.9159C6.97843 12.0721 6.66622 12.0721 6.51012 11.9159L0.994441 6.40027C0.786302 6.19213 0.370025 6.19213 0.161886 6.40027C-0.0462532 6.60841 0.00578167 6.66044 0.00578167 6.81655C0.00578167 6.97265 0.0578162 7.12876 0.161886 7.23283L6.40605 13.477C6.51012 13.5811 6.66622 13.6331 6.82233 13.6331C6.97843 13.6331 7.13454 13.5811 7.23861 13.477L19.7269 0.98866C19.831 0.88459 19.883 0.728486 19.883 0.572382C19.883 0.416278 19.831 0.260174 19.7269 0.156104Z" fill="#232323"/>
</svg>

After

Width:  |  Height:  |  Size: 669 B

5
src/assets/icons/eye.svg Normal file
View File

@@ -0,0 +1,5 @@
<svg width="19" height="13" viewBox="0 0 19 13" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.5 6.49998C3.33333 1.97224 10.9 -4.36659 18.5 6.49998" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M18.5 6.49998C15.6667 11.0277 8.1 17.3666 0.5 6.49998" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="9.5" cy="6.49998" r="2.5" stroke="#232323"/>
</svg>

After

Width:  |  Height:  |  Size: 432 B

BIN
src/assets/images/edit.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 928 B

View File

@@ -7,9 +7,11 @@ export default {
name: 'Name',
email: 'Email',
password: 'Password',
passwordConfirmation: 'Password Confirmation',
enterName: 'Enter your name',
enterEmail: 'Enter your email',
enterPassword: 'Enter your password',
enterPasswordAgain: 'Enter your password again',
forgotPassword: 'Forget password?',
pleaseInputName: 'Please input the name',
nameLengthError: 'Name length must be between {min} and {max} characters',
@@ -28,7 +30,7 @@ export default {
havenAccountToLogin: `Already have an account? <span onclick="onClickLogin()">Log in</span>`,
verifyEmail: 'Verify your email address',
verifyCodeHasSent: 'A verification code has been sent to<br><span>{email}</span>',
verify: 'Verify',
verify: 'VERIFY',
resendCode: 'Resend Code',
resendCodeIn: 'Resend Code in {time}',
orContinueWith: 'or continue with',

View File

@@ -8,9 +8,11 @@ export default {
name: '姓名',
email: '邮箱',
password: '密码',
passwordConfirmation: '密码确认',
enterName: '请输入姓名',
enterEmail: '请输入邮箱',
enterPassword: '请输入密码',
enterPasswordAgain: '请输入密码确认',
forgotPassword: '忘记密码?',
pleaseInputName: '请输入姓名',
nameLengthError: '姓名长度必须在 {min} 到 {max} 个字符之间',

View File

@@ -7,42 +7,46 @@ import { createRouter, createWebHistory } from 'vue-router'
* 3. 路由的name默认是文件名,如果文件名与name不一致,通过defineOptions({ name: 'componentName' })来设置
*/
const router = createRouter({
history: createWebHistory('/'),
// history: createWebHistory(import.meta.env.VITE_APP_URL),
routes: [
{
path: '/',
name: 'home',
component: () => import('../views/home/index.vue'),
},
{
path: '/collectionStory',
name: 'collectionStory',
component: () => import('../views/collectionStory/index.vue'),
},
{
path: '/brand',
name: 'brand',
component: () => import('../views/brand/index.vue'),
},
{
path: '/digitalItem',
name: 'digitalItem',
component: () => import('../views/digitalItem/index.vue'),
},
{
path: '/:pathMatch(.*)',
name: '404',
component: () => import('../views/404.vue'),
},
]
routes: [
{
path: '/',
name: 'home',
component: () => import('../views/home/index.vue')
},
{
path: '/collectionStory',
name: 'collectionStory',
component: () => import('../views/collectionStory/index.vue')
},
{
path: '/brand',
name: 'brand',
component: () => import('../views/brand/index.vue')
},
{
path: '/digitalItem',
name: 'digitalItem',
component: () => import('../views/digitalItem/index.vue'),
},
{
path: '/settings',
name: 'settings',
component: () => import('@/views/setting/index.vue'),
meta: { cache: true }
},
{
path: '/:pathMatch(.*)',
name: '404',
component: () => import('../views/404.vue')
}
],
history: createWebHistory('/')
})
router.beforeEach((to, from, next) => {
next()
next()
})
router.afterEach(() => {
})
router.afterEach(() => {})
export default router

View File

@@ -38,7 +38,7 @@
--el-input-border-radius: 0;
--el-input-text-color: #232323;
--el-border-color: #C4C4C4;
font-size: 1.4rem;
font-size: 1rem;
}
.retrieve-password:deep(.el-form) .el-input::placeholder,
.register:deep(.el-form) .el-input::placeholder,

View File

@@ -1,5 +1,5 @@
<template>
<div class="visible-code">
<div class="email-verify">
<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>
@@ -7,7 +7,7 @@
<span @click="onResend">{{ $t('Login.resendCode') }}</span>
</p>
<button class="verify" custom="black" @click="onVerify">{{ $t('Login.verify') }}</button>
<other-login />
<other-login v-if="isShowOtherLogin" />
</div>
</template>
@@ -29,7 +29,8 @@
type: String as () => 'LOGIN' | 'REGISTER' | 'FORGOT_PWD',
required: true
},
password: { type: String, default: '' }
password: { type: String, default: '' },
isShowOtherLogin: { type: Boolean, default: true }
})
const code = ref('')
const time = ref(60)
@@ -96,7 +97,7 @@
</script>
<style lang="less" scoped>
.visible-code {
.email-verify {
width: 100%;
display: flex;
flex-direction: column;
@@ -123,17 +124,15 @@
--button-font-size: 1.4rem;
}
> .time {
font-family: KaiseiOpti-Regular;
user-select: none;
margin-top: 2.4rem;
font-size: 1.6rem;
font-size: 1.2rem;
color: #666;
font-family: Regular;
> span {
color: #ff7a50;
color: #232323;
text-decoration: underline;
cursor: pointer;
font-weight: 500;
font-family: Medium;
}
}
> .other-login {

View File

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

@@ -32,7 +32,7 @@
--el-input-border-radius: 0;
--el-input-text-color: #232323;
--el-border-color: #C4C4C4;
font-size: 1.4rem;
font-size: 1rem;
&::placeholder {
color: #9F9F9F;

View File

@@ -10,7 +10,6 @@
: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">
@@ -22,8 +21,10 @@
<div class="nav" v-show="!curentTabInfo.title">
<div
class="item"
:class="{ active: currentTab === TabNames.sign_up }"
@click="currentTab = TabNames.sign_up"
:class="{
active: [TabNames.register, TabNames.register_success].includes(currentTab)
}"
@click="currentTab = TabNames.register"
>
SIGN UP
</div>
@@ -42,6 +43,7 @@
@login="onLogin"
@register="onRegister"
@submit-email-code="onSubmitEmailCode"
@back="onBack"
:name="data.name"
:email="data.email"
:password="data.password"
@@ -58,6 +60,8 @@
import login from './login.vue'
import register from './register.vue'
import emailVerify from './email-verify.vue'
import registerSuccess from './register-success.vue'
import retrievePassword from './retrieve-password.vue'
import myEvent from '@/utils/myEvent'
const data = ref({
name: '',
@@ -65,11 +69,11 @@
password: '',
type: ''
})
const show = ref(false)
const TabNames = {
login: 'login',
sign_up: 'sign_up',
register: 'register',
email_verify: 'email_verify',
register_success: 'register_success',
retrieve_password: 'retrieve_password'
}
const tabList = markRaw([
@@ -78,7 +82,7 @@
component: login
},
{
name: TabNames.sign_up,
name: TabNames.register,
component: register
},
{
@@ -89,15 +93,20 @@
{
name: TabNames.retrieve_password,
title: 'RETRIEVE PASSWORD',
component: login
component: retrievePassword
},
{
name: TabNames.register_success,
component: registerSuccess
}
])
const show = ref(false)
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
currentTab.value = lastTab.value || TabNames.login
}
const open = (type?: string) => {
currentTab.value = TabNames[type] || TabNames.login
@@ -118,13 +127,17 @@
}
const onRegister = (res: any) => {
data.value = res
data.value.type = TabNames.sign_up
data.value.type = TabNames.register
currentTab.value = TabNames.email_verify
}
const onSubmitEmailCode = (code: string) => {
// data.value.code = code
console.log(code)
show.value = false
if (data.value.type === TabNames.login) {
console.log('登录', code)
show.value = false
} else {
console.log('注册', code)
currentTab.value = TabNames.register_success
}
}
</script>
@@ -147,14 +160,16 @@
height: 100%;
overflow: hidden;
position: relative;
background: url('@/assets/images/login/bg.jpg') no-repeat center center / 100% 100%;
> *:not(.content) {
position: absolute;
}
> .bg {
width: 100%;
height: 100%;
display: block;
}
> * {
position: absolute;
}
> .logo {
width: auto;
height: 4rem;
@@ -170,9 +185,10 @@
}
> .content {
width: 34rem;
top: 5rem;
right: 6rem;
height: calc(100% - 10rem);
margin: 5rem 6rem auto auto;
display: flex;
flex-direction: column;
> .header {
--padding-bottom: 1.2rem;
padding-bottom: var(--padding-bottom);

View File

@@ -40,15 +40,15 @@
.password-tip {
background: #404040;
color: #fff;
font-size: 1.4rem;
padding: 2rem;
border-radius: 2rem;
font-size: 1.1rem;
padding: 1.5rem;
border-radius: 1.5rem;
line-height: normal;
> div {
display: flex;
align-items: center;
margin-bottom: 0.5rem;
margin-bottom: 1rem;
&:last-child {
margin-bottom: 0;
}

View File

@@ -0,0 +1,113 @@
<template>
<div class="register-success">
<div class="icon"><svg-icon name="dui" size="20" /></div>
<div class="title">Welcome to Stylish Parade!</div>
<div class="title">Please switch to the Login tab to log in.</div>
<div class="footer">
<div class="title">
<span class="text">What awaits you in Stylish Parade</span>
<span class="icon"><svg-icon name="arrow_right" size="11" /></span>
</div>
<div class="content">
<div>
<div class="title">Behind the design</div>
<div class="tip">
Discover how designers bring ideas to life with AiDA from first sketch to final look.
</div>
</div>
<div>
<div class="title">Creative digital works</div>
<div class="tip">
Unlock a growing library of inspiring digital works to refresh your creative mind.
</div>
</div>
<div>
<div class="title">A fashion community</div>
<div class="tip">
Join a space where fashion speaks exchange ideas and connect with creators worldwide.
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
</script>
<style lang="less" scoped>
.register-success {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
margin-top: 7.7rem;
> .icon {
width: 4rem;
height: 4rem;
border-radius: 50%;
background-color: #fff;
border: 0.1rem solid #e8e8e8;
margin-bottom: 1.8rem;
}
> .title {
font-size: 1.6rem;
line-height: 2.4rem;
text-align: center;
color: #232323;
}
> .footer {
position: absolute;
width: 100%;
left: 0;
bottom: 7rem;
padding: 0 6rem;
> .title {
font-family: KaiseiOpti-Regular;
font-size: 1.4rem;
line-height: 2.4rem;
color: #666;
display: flex;
align-items: center;
justify-content: flex-end;
margin-bottom: 1.2rem;
> .icon {
margin-left: 1.2rem;
transform: rotate(90deg);
}
}
> .content {
display: flex;
align-items: center;
justify-content: center;
gap: 1.8rem;
> div {
padding: 2.4rem 1.5rem 0;
height: 14.8rem;
background: rgba(255, 255, 255, 0.9);
box-shadow: 3px 4px 8px 0px rgba(0, 0, 0, 0.11);
flex: 1;
&:first-child {
flex: 0.84;
}
> .title {
font-family: KaiseiOpti-Bold;
font-size: 1.6rem;
line-height: 2.4rem;
margin-bottom: 1.2rem;
color: #232323;
}
> .tip {
font-family: KaiseiOpti-Regular;
font-size: 1.2rem;
line-height: 1.7rem;
color: #585858;
}
}
}
}
}
</style>

View File

@@ -1,120 +1,183 @@
<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" />
<el-form
:model="formData"
:rules="ruleForm"
label-position="top"
ref="form1Ref"
v-show="index === 0"
>
<div class="title">Please enter your email address below to verify your identity.</div>
<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 class="submit-item">
<button class="submit" type="submit" custom="black" @click.prevent="onSubmit1">
SUBMIT
</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>
</el-form-item>
</el-form>
<div class="verify-box" v-if="index === 1">
<email-verify
type="FORGOT_PWD"
:email="formData.email"
@submit-email-code="onVerifyCode"
:is-show-other-login="false"
/>
</div>
<el-form
:model="formData"
:rules="ruleForm"
label-position="top"
ref="form2Ref"
v-show="index === 2"
>
<div class="title">
Enter a new password for <br />
<span>{{ formData.email }}</span>
</div>
<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>
<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.passwordConfirmation')" prop="confirmPassword">
<el-input
v-model="formData.confirmPassword"
:placeholder="$t('Login.enterPasswordAgain')"
type="password"
show-password
name="password"
/>
</el-form-item>
<el-form-item class="submit-item">
<button class="submit" type="submit" custom="black" @click.prevent="onSubmit2">
SUBMIT
</button>
</el-form-item>
</el-form>
</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()
import EmailVerify from './email-verify.vue'
const emit = defineEmits(['back'])
const validateConfirmPassword = (rule: any, value: string, callback: any) => {
if (value !== formData.password) {
callback(new Error('Passwords do not match'))
} else {
callback()
}
}
const index = ref(0)
const ruleForm = reactive({
email: [{ validator: validateEmail, trigger: 'change' }],
password: [{ validator: validatePass, trigger: 'change' }]
password: [{ validator: validatePass, trigger: 'change' }],
confirmPassword: [{ validator: validateConfirmPassword, trigger: 'change' }]
})
const isVisible = ref(false)
const showPasswordTip = ref(false)
const formData = reactive({
email: '',
password: ''
code: '',
password: '',
confirmPassword: ''
})
const formRef = ref(null)
const onBack = () => {
if (isVisible.value) {
isVisible.value = false
} else {
router.back()
}
}
const form1Ref = ref(null)
const visibleCodeRef = ref(null)
const onSubmit = () => {
formRef.value?.validate?.((valid) => {
const form2Ref = ref(null)
const onSubmit1 = () => {
form1Ref.value?.validate?.((valid) => {
if (valid) {
// console.log('submit!')
visibleCodeRef.value?.onSendCode().then(() => {
isVisible.value = true
})
index.value = 1
} else {
console.warn('error submit!')
}
})
}
const onSubmit2 = () => {
form2Ref.value?.validate?.((valid) => {
if (valid) {
const data = {
email: formData.email,
code: formData.code,
password: md5(formData.password)
}
console.log(data)
emit('back')
} 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)
})
if (!code) return
formData.code = code
index.value = 2
}
</script>
<style lang="less" scoped>
@import './less/style.less';
.retrieve-password {
flex: 1;
&:deep(.el-form) {
height: 100%;
display: flex;
flex-direction: column;
.el-form-item.submit-item {
margin-top: auto;
}
.el-input {
--el-input-height: 4.8rem;
}
.el-form-item:nth-last-child(2) {
margin-bottom: 10rem;
}
> .title {
font-family: KaiseiOpti-Regular;
font-size: 1.6rem;
line-height: 2.4rem;
text-align: center;
color: #585858;
margin-top: auto;
margin-bottom: 3rem;
> span {
font-family: KaiseiOpti-Medium;
color: #252727;
}
}
}
> .verify-box {
width: 100%;
height: 100%;
display: flex;
&:deep(.email-verify) {
> .tip {
margin-top: 8rem;
}
> .input-code {
margin-top: 3rem;
}
> .verify {
margin-top: auto;
}
}
}
}
</style>

View File

@@ -124,7 +124,7 @@
}
const onSettings = () => {
hideProfilePopover()
console.log('settings')
router.push('/settings')
}
const onLogout = () => {
hideProfilePopover()

View File

@@ -0,0 +1,103 @@
<template>
<div class="radio-button-group">
<button
v-for="item in options"
:key="item.value"
type="button"
:class="[
'radio-button',
{
'is-active': multiple ? selectedValues.includes(item.value) : modelValue === item.value
}
]"
@click="selectOption(item.value)"
>
{{ item.name }}
</button>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
interface Option {
name: string | number
value: string | number | boolean
}
const props = defineProps<{
modelValue: string | number | boolean | Array<string | number | boolean> | null
options: Option[] // 按钮选项数组
multiple?: boolean // 是否支持多选,默认为 false
max?: number // 多选时最多可选数量,不传则不限制
}>()
const emit = defineEmits<{
(e: 'update:modelValue', value: any): void
}>()
const multiple = props.multiple === true
const selectedValues = computed(() => {
if (!multiple) {
return typeof props.modelValue === 'undefined' || props.modelValue === null
? []
: [props.modelValue]
}
return Array.isArray(props.modelValue) ? props.modelValue : []
})
const selectOption = (value: any) => {
if (multiple) {
const current = Array.isArray(props.modelValue) ? [...props.modelValue] : []
const index = current.indexOf(value)
if (index >= 0) {
current.splice(index, 1)
} else {
if (typeof props.max === 'number' && props.max > 0 && current.length >= props.max) {
current.shift()
}
current.push(value)
}
emit('update:modelValue', current)
return
}
if (props.modelValue !== value) {
emit('update:modelValue', value)
}
}
</script>
<style lang="less" scoped>
.radio-button-group {
display: flex;
gap: 0.8rem;
flex-wrap: wrap;
}
.radio-button {
border: 1px solid #979797;
height: 4rem;
min-width: 8rem;
padding: 0 2rem;
color: #979797;
cursor: pointer;
outline: none;
background-color: #fff;
font-size: 1.4rem;
}
.radio-button:hover {
border-color: #000;
color: #fff;
background-color: #000;
}
.radio-button.is-active {
color: #fff;
background-color: #000;
border-color: #000;
}
</style>

798
src/views/setting/index.vue Normal file
View File

@@ -0,0 +1,798 @@
<template>
<div class="setting-wrapper mini-scrollbar">
<div class="banner flex flex-center flex-col">
<div class="title">Settings</div>
<div class="slogan">Manage your account settings and preferences</div>
</div>
<div class="setting-content">
<div class="setting-item flex">
<div class="label-container">
<div class="label">Profile</div>
<div class="label-desc">
Update your display name, avatar, social links and account security.
</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">FIRST NAME</div>
<div v-show="!isEditing" class="read-box">{{ displayData.firstName }}</div>
<div v-show="isEditing" class="form-item-value name">
<el-input v-model="draftData.firstName" placeholder="First Name" />
</div>
</div>
<div class="read-item">
<div class="read-label">LAST NAME</div>
<div v-show="!isEditing" class="read-box">{{ displayData.lastName }}</div>
<div v-show="isEditing" class="form-item-value name">
<el-input v-model="draftData.lastName" placeholder="Last Name" />
</div>
</div>
</div>
<div class="read-row">
<div class="read-label">USERNAME</div>
<div v-show="!isEditing" class="read-box">{{ displayData.username }}</div>
<div v-show="isEditing" class="form-item-value">
<el-input v-model="draftData.username" placeholder="Username" />
</div>
</div>
<div class="read-tip">Your public username on Stylish Parade.</div>
<div class="read-row role-row">
<div class="read-label">ROLE</div>
<div :class="{ 'readonly-radio-group': !isEditing }">
<Radio multiple :max="2" v-model="roleModel" :options="roleList" />
</div>
</div>
<div class="read-tip">Select up to 2 labels that suit you.</div>
<div class="social-links read-social-links">
<div class="title">SOCIAL LINKS</div>
<div class="links-list flex flex-col">
<div
class="links-item flex align-center"
v-for="(item, index) in displayData.links"
:key="`view-${index}`"
>
<div class="link-index">Link {{ index + 1 }}</div>
<div v-show="!isEditing" class="link-href flex-1 readonly">{{ item }}</div>
<div v-show="isEditing" class="link-href flex-1">
<el-input v-model="draftData.links[index]" />
</div>
</div>
<button
v-show="isEditing"
type="button"
class="add-link-btn"
@click="handleAddLink"
>
+
</button>
</div>
</div>
</div>
</div>
</div>
<div class="gap" />
<div class="setting-item flex">
<div class="label-container">
<div class="label">Security</div>
<div class="label-desc">Manage your login email and password.</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">EMAIL</div>
<div class="security-static flex-1">{{ displayData.email }}</div>
<button
v-show="isEditing"
type="button"
class="small-btn"
@click="resetSecurityEmail"
>
CANCEL
</button>
</div>
</div>
<div v-show="isEditing" class="security-row">
<div class="security-label">NEW EMAIL ADDRESS</div>
<div class="outlined-field verify-field align-center">
<el-input v-model="securityDraft.newEmail" placeholder="Enter new email" />
<div class="verify-btn">Verify</div>
</div>
</div>
<div class="inner-divider" />
<div class="security-row">
<div class="security-inline-row flex align-center">
<div class="security-label inline">PASSWORD</div>
<div class="security-static password-mask flex-1">.........</div>
<button
v-show="isEditing"
type="button"
class="small-btn"
@click="resetSecurityPassword"
>
CANCEL
</button>
</div>
</div>
<div v-show="isEditing" class="security-row">
<div class="security-label">NEW PASSWORD</div>
<div class="outlined-field">
<el-input
v-model="securityDraft.newPassword"
type="password"
show-password
placeholder="Enter new password"
/>
</div>
<div class="security-tip">You must satisfy ALL password conditions to register.</div>
</div>
<div v-show="isEditing" class="security-row">
<div class="security-label">CURRENT PASSWORD</div>
<div class="outlined-field">
<el-input
v-model="securityDraft.currentPassword"
type="password"
show-password
placeholder="Confirm with your password"
/>
</div>
</div>
<div class="inner-divider" />
</div>
</div>
<div class="gap" />
<div class="setting-item flex">
<div class="label-container">
<div class="label">Language &amp; Region</div>
<div class="label-desc">Set your preferred language, region and currency display.</div>
</div>
<div class="context-container region-container">
<div class="region-row">
<div class="security-label">DISPLAY LANGUAGE</div>
<div v-show="!isEditing" class="security-static field-box">
{{ displayData.language }}
</div>
<div v-show="isEditing" class="outlined-field select-field">
<el-select v-model="draftData.language" placeholder="Select language">
<el-option v-for="item in languageList" :key="item" :label="item" :value="item" />
</el-select>
</div>
</div>
<div class="region-row">
<div class="security-label">REGION</div>
<div v-show="!isEditing" class="security-static field-box">
{{ displayData.region }}
</div>
<div v-show="isEditing" class="outlined-field select-field">
<el-select v-model="draftData.region" placeholder="Select region">
<el-option v-for="item in regionList" :key="item" :label="item" :value="item" />
</el-select>
</div>
</div>
</div>
</div>
<div class="gap bottom-gap" />
<div class="action-container flex">
<template v-if="isEditing">
<button type="button" class="primary-btn" :disabled="saving" @click="handleSave">
{{ saving ? 'SAVING...' : 'SAVE CHANGE' }}
</button>
<button type="button" class="secondary-btn" :disabled="saving" @click="handleDiscard">
DISCARD
</button>
</template>
<template v-else>
<button type="button" class="primary-btn edit-btn" @click="handleEdit">EDIT</button>
</template>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import { ElMessage } from 'element-plus'
import Radio from './components/Radio.vue'
interface SettingsData {
firstName: string
lastName: string
email: string
username: string
role: string[]
links: string[]
language: string
region: string
}
interface SecurityDraft {
newEmail: string
newPassword: string
currentPassword: string
}
const roleList = [
'Fashion Enthusiast',
'Content Creator',
'Student',
'Retail / Buyer',
'Fashion Designer',
'Brand / Business',
'PR & Communications',
'Stylist',
'Graphic Designer',
'3D Artist',
'Other'
].map((item) => ({ name: item, value: item }))
const languageList = ['English', 'Chinese', 'Japanese']
const regionList = ['Hong Kong SAR', 'Mainland China', 'Singapore', 'United Kingdom']
const createDefaultData = (): SettingsData => ({
firstName: 'Alexandra',
lastName: 'Chen',
email: 'alex.chen@gmail.com',
username: '@alexandra_chen',
role: ['Student', 'Graphic Designer'],
links: ['https://instagram.com/username', 'https://...'],
language: 'English',
region: 'Hong Kong SAR'
})
const cloneSettingsData = (data: SettingsData): SettingsData => ({
firstName: data.firstName,
lastName: data.lastName,
email: data.email,
username: data.username,
role: [...data.role],
links: [...data.links],
language: data.language,
region: data.region
})
const createEmptySecurityDraft = (): SecurityDraft => ({
newEmail: '',
newPassword: '',
currentPassword: ''
})
const sourceData = ref<SettingsData>(createDefaultData())
const draftData = ref<SettingsData>(cloneSettingsData(sourceData.value))
const securityDraft = ref<SecurityDraft>(createEmptySecurityDraft())
const isEditing = ref(false)
const saving = ref(false)
const displayData = computed(() => (isEditing.value ? draftData.value : sourceData.value))
const fullName = computed(() => {
const data = displayData.value
return `${data.firstName} ${data.lastName}`.trim()
})
const roleModel = computed({
get: () => displayData.value.role,
set: (value: string[]) => {
if (isEditing.value) {
draftData.value.role = value
return
}
sourceData.value.role = value
}
})
const resetDraftState = () => {
draftData.value = cloneSettingsData(sourceData.value)
securityDraft.value = createEmptySecurityDraft()
}
const handleEdit = () => {
resetDraftState()
isEditing.value = true
}
const handleAddLink = () => {
draftData.value.links.push('')
}
const resetSecurityEmail = () => {
securityDraft.value.newEmail = ''
}
const resetSecurityPassword = () => {
securityDraft.value.newPassword = ''
securityDraft.value.currentPassword = ''
}
const handleDiscard = () => {
resetDraftState()
isEditing.value = false
}
const buildNextData = () => {
const nextEmail = securityDraft.value.newEmail.trim() || draftData.value.email
return {
firstName: draftData.value.firstName.trim(),
lastName: draftData.value.lastName.trim(),
username: draftData.value.username.trim(),
email: nextEmail,
role: draftData.value.role,
links: draftData.value.links.map((item) => item.trim()).filter(Boolean),
language: draftData.value.language,
region: draftData.value.region
}
}
const handleSave = async () => {
const nextData = buildNextData()
saving.value = true
try {
sourceData.value = cloneSettingsData({
...nextData,
links: nextData.links.length ? nextData.links : ['']
})
draftData.value = cloneSettingsData(sourceData.value)
securityDraft.value = createEmptySecurityDraft()
isEditing.value = false
ElMessage.success('Settings updated')
} catch (error) {
console.warn(error)
} finally {
saving.value = false
}
}
</script>
<style lang="less" scoped>
.setting-wrapper {
height: 100%;
overflow-y: auto;
background: #ffffff;
.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;
}
.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%);
.title {
font-family: 'KaiseiOpti-Bold';
font-size: 4rem;
line-height: 3.6rem;
color: #232323;
}
.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;
}
.social-links {
margin-top: 5.8rem;
font-family: 'KaiseiOpti-Medium';
.title {
margin-bottom: 2rem;
font-size: 1.4rem;
color: #585858;
}
&.read-social-links {
margin-top: 4.8rem;
}
}
.links-list {
row-gap: 0.8rem;
}
.links-item {
column-gap: 3.4rem;
}
.link-index {
font-family: 'KaiseiOpti-Regular';
font-size: 1.4rem;
line-height: 2rem;
color: #979797;
}
.link-href {
color: #979797;
&.readonly {
display: flex;
align-items: center;
padding: 0.8rem 2rem;
font-family: 'KaiseiOpti-Regular';
font-size: 1.4rem;
line-height: 2rem;
color: #9f9f9f;
}
:deep(.el-input) {
height: 4rem;
}
:deep(.el-input__inner) {
font-family: 'KaiseiOpti-Regular';
font-size: 1.4rem;
color: #9f9f9f;
}
}
.add-link-btn {
width: 4rem;
height: 4rem;
border: 0.1rem solid #f0ebe5;
background: #f6f6f6;
color: #b4aea6;
font-size: 2rem;
line-height: 1;
cursor: pointer;
}
.security-container {
.security-row + .security-row {
margin-top: 2.8rem;
}
.security-label {
margin: 0 0 0.8rem;
&.inline {
width: 10.8rem;
margin-bottom: 0;
flex-shrink: 0;
}
}
.security-static {
min-height: 2.4rem;
padding: 0.1rem 0 0;
border: none;
}
.security-inline-row {
gap: 2.8rem;
min-height: 3.2rem;
}
.security-tip {
margin-top: 0.6rem;
}
.verify-field {
display: flex;
margin-top: 0.8rem;
:deep(.el-input) {
flex: 1;
}
}
.verify-btn {
border: none;
height: 2.8rem;
line-height: 2.8rem;
border-left: 0.1rem solid #979797;
background: #ffffff;
font-family: 'KaiseiOpti-Medium';
font-size: 1.4rem;
color: #232323;
cursor: pointer;
padding: 0 2rem;
}
.password-mask {
font-family: 'KaiseiOpti-Bold';
letter-spacing: 0.08rem;
}
.inner-divider {
height: 1px;
margin: 2rem 0;
background-color: #c4c4c4;
}
}
.region-container {
.field-box {
padding: 0.8rem 2rem;
}
}
.small-btn,
.secondary-btn,
.primary-btn {
border: 0.1rem solid #c4c4c4;
background: #f6f6f6;
font-family: 'KaiseiOpti-Bold';
color: #232323;
cursor: pointer;
}
.small-btn {
width: 10rem;
height: 3.2rem;
align-self: flex-start;
font-size: 1.2rem;
line-height: 2.6rem;
letter-spacing: -0.03em;
}
.gap {
height: 0.05rem;
margin-top: 6rem;
margin-bottom: 4rem;
background-color: #c4c4c4;
&.bottom-gap {
margin-top: 4rem;
}
}
.action-container {
justify-content: center;
column-gap: 1.2rem;
margin-top: 2.8rem;
}
.primary-btn,
.secondary-btn {
height: 4.4rem;
font-size: 1.6rem;
line-height: 2.6rem;
&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
}
.primary-btn {
min-width: 23.6rem;
padding: 0 2.4rem;
border-color: #232323;
background: #232323;
color: #ffffff;
}
.secondary-btn {
width: 12rem;
background: #ffffff;
}
.edit-btn {
min-width: 12rem;
}
}
}
:deep(.el-select-dropdown__item) {
padding: 0 2rem !important;
}
</style>